chore: initial release v0.1.0

Runtime-agnostic oneshot broadcast channel for Rust.
This commit is contained in:
swaits committed 2025-10-13 19:36:37 -06:00
commit e8e55b7676
64 files changed
+23966

No files matched your search

+308
View File
@@ -0,0 +1,308 @@
//! ============================================================================
//! LEARNING GUIDE - PART 4: Efficient Broadcasting with Arc
//! ============================================================================
//!
//! WHAT YOU'LL LEARN:
//! - Why Arc<T> is important for efficient broadcasting
//! - How to avoid expensive clones with large data
//! - Memory efficiency with Arc reference counting
//! - When to use Arc<T> vs direct values
//!
//! PREREQUISITES:
//! - Read examples 01, 02, and 03 first
//! - Understand basic Rust Arc (shared ownership)
//! - Understand Clone trait
//!
//! KEY CONCEPTS:
//! - T: Clone requirement means each listener clones the value
//! - For large types, cloning is expensive
//! - Arc<T>: Clone just increments refcount (cheap!)
//! - Memory: One allocation, many references
//!
//! Run with: cargo run --example 04_efficient_broadcasting
use announcement::Announcement;
use std::sync::Arc;
// Define some example types of different sizes
#[derive(Clone, Debug, PartialEq)]
struct SmallData {
value: i32,
}
#[derive(Clone, Debug, PartialEq)]
#[allow(dead_code)]
struct MediumData {
buffer: [u8; 1024], // 1 KB
}
#[derive(Clone, Debug, PartialEq)]
struct LargeData {
buffer: Vec<u8>, // Could be many MB
metadata: String,
}
fn main() {
println!("=== Efficient Broadcasting with Arc ===\n");
// STEP 1: The Clone requirement
// ==============================
// Remember: Announcement requires T: Clone
// This means each listener gets a CLONED copy of the value.
//
// For small types (like i32), this is fine:
// - i32: 4 bytes, copying is trivial
//
// For large types, this can be expensive:
// - Vec<u8> with 10MB: Cloning allocates 10MB per listener!
println!("Step 1: Understanding the Clone requirement\n");
println!(" Announcement<T> requires T: Clone");
println!(" Each listener.try_listen() or .listen().await CLONES T");
println!(" For large T, this can be expensive!\n");
// STEP 2: Example - Small data WITHOUT Arc
// =========================================
// For small types, direct usage is fine and efficient.
println!("Step 2: Small data WITHOUT Arc (efficient):");
let (announcer, announcement) = Announcement::<SmallData>::new();
let small_data = SmallData { value: 42 };
println!(
" Size of SmallData: {} bytes",
std::mem::size_of::<SmallData>()
);
println!(" Creating 3 listeners...");
let l1 = announcement.listener();
let l2 = announcement.listener();
let l3 = announcement.listener();
announcer.announce(small_data.clone()).unwrap();
let r1 = l1.try_listen().unwrap();
let _r2 = l2.try_listen().unwrap();
let _r3 = l3.try_listen().unwrap();
println!(" All listeners received: value={}", r1.value);
println!(" Cost: 3 clones of 4 bytes each = 12 bytes total");
println!(" Verdict: ✓ Direct usage is fine for small types\n");
// STEP 3: Example - Large data WITHOUT Arc (INEFFICIENT!)
// ========================================================
// For large types, direct cloning wastes memory and time.
println!("Step 3: Large data WITHOUT Arc (INEFFICIENT):");
let (announcer, announcement) = Announcement::<LargeData>::new();
// Create a 1MB buffer
let large_buffer = vec![42u8; 1024 * 1024]; // 1 MB
let large_data = LargeData {
buffer: large_buffer,
metadata: "This is large data".to_string(),
};
let data_size = large_data.buffer.len();
println!(" Size of LargeData: ~{} bytes", data_size);
println!(" Creating 3 listeners...");
let l1 = announcement.listener();
let l2 = announcement.listener();
let l3 = announcement.listener();
announcer.announce(large_data).unwrap();
let r1 = l1.try_listen().unwrap();
let _r2 = l2.try_listen().unwrap();
let _r3 = l3.try_listen().unwrap();
println!(" All listeners received {} byte buffer", r1.buffer.len());
println!(
" Cost: 3 clones of {} bytes each = {} bytes total",
data_size,
data_size * 3
);
println!(" Verdict: ✗ WASTEFUL - we allocated 3MB when we only need 1MB!\n");
// STEP 4: The Solution - Arc<T>
// ==============================
// Wrap large types in Arc to avoid expensive clones.
//
// IMPORTANT: Arc<T>: Clone just increments a reference count!
// - Original data: 1 allocation (1MB in this example)
// - Each clone: Just increment counter (~5ns, no allocation)
// - Memory: Shared, not duplicated
println!("Step 4: Large data WITH Arc (EFFICIENT!):");
// NOTE: We wrap LargeData in Arc
let (announcer, announcement) = Announcement::<Arc<LargeData>>::new();
let large_buffer = vec![99u8; 1024 * 1024]; // 1 MB
let large_data = Arc::new(LargeData {
buffer: large_buffer,
metadata: "This is Arc'd large data".to_string(),
});
println!(" Size of wrapped data: ~{} bytes", large_data.buffer.len());
println!(
" Size of Arc pointer: {} bytes",
std::mem::size_of::<Arc<LargeData>>()
);
println!(" Creating 3 listeners...");
let l1 = announcement.listener();
let l2 = announcement.listener();
let l3 = announcement.listener();
// IMPORTANT: We clone the Arc before announcing, to keep a reference
announcer.announce(Arc::clone(&large_data)).unwrap();
let r1 = l1.try_listen().unwrap();
let r2 = l2.try_listen().unwrap();
let r3 = l3.try_listen().unwrap();
println!(" All listeners received {} byte buffer", r1.buffer.len());
println!(
" Cost: 3 clones of Arc pointer = {} bytes total",
std::mem::size_of::<Arc<LargeData>>() * 3
);
println!(" Verdict: ✓ EFFICIENT - only 1MB allocated, shared among all!\n");
// STEP 5: Verifying Arc refcount
// ===============================
// Arc keeps a reference count. Let's verify memory is shared.
println!("Step 5: Verifying Arc reference counting:");
println!(
" Original Arc refcount: {}",
Arc::strong_count(&large_data)
);
println!(" Expected: 5 (original + announcement + r1 + r2 + r3)");
// NOTE: Actually we have:
// - original (large_data variable)
// - stored in announcement's OnceLock
// - r1, r2, r3 (cloned by listeners)
// = 5 total references
let actual_count = Arc::strong_count(&r1);
println!(" Actual count via r1: {}", actual_count);
if actual_count == 5 {
println!(" ✓ Correct! All references point to the same allocation\n");
}
// Verify all Arc's point to the same data
println!(" Verifying all listeners have same data:");
println!(" r1.metadata: {}", r1.metadata);
println!(" r2.metadata: {}", r2.metadata);
println!(" r3.metadata: {}", r3.metadata);
println!(" Arc::ptr_eq(&r1, &r2): {}", Arc::ptr_eq(&r1, &r2));
println!(" ✓ Same memory address - truly shared!\n");
// STEP 6: Performance comparison
// ===============================
println!("Step 6: Performance comparison:");
println!("\n Direct Clone (1MB data, 100 listeners):");
println!(" - Memory: 100 MB (100 allocations)");
println!(" - Time: ~100ms (100 × 1ms per clone)");
println!(" - CPU cache: Poor (scattered allocations)");
println!("\n Arc Clone (1MB data, 100 listeners):");
println!(" - Memory: 1 MB (1 allocation)");
println!(" - Time: ~500ns (100 × 5ns per refcount)");
println!(" - CPU cache: Good (shared memory)");
println!("\n Speedup: ~200,000x faster!");
println!(" Memory saved: 99 MB\n");
// STEP 7: When to use Arc
// ========================
println!("Step 7: Decision guide - When to use Arc:");
println!("\n Use DIRECT value when:");
println!(" ✓ Type is small (< 16 bytes)");
println!(" ✓ Type is Copy (i32, f64, etc.)");
println!(" ✓ Only 1-2 listeners");
println!(" ✓ Clone is cheap");
println!("\n Use Arc<T> when:");
println!(" ✓ Type is large (> 16 bytes)");
println!(" ✓ Clone is expensive");
println!(" ✓ Many listeners (3+)");
println!(" ✓ Type contains Vec, String, HashMap, etc.");
println!("\n RULE OF THUMB:");
println!(" For > 1 listener AND size > 8 bytes: Use Arc<T>\n");
// STEP 8: Example with complex type
// ==================================
#[derive(Clone, Debug)]
#[allow(dead_code)]
struct Config {
database_url: String,
api_keys: Vec<String>,
settings: std::collections::HashMap<String, String>,
}
println!("Step 8: Real-world example - Configuration broadcast:");
let (announcer, announcement) = Announcement::<Arc<Config>>::new();
let config = Arc::new(Config {
database_url: "postgresql://localhost/mydb".to_string(),
api_keys: vec!["key1".to_string(), "key2".to_string(), "key3".to_string()],
settings: [
("timeout".to_string(), "30".to_string()),
("retry".to_string(), "3".to_string()),
]
.iter()
.cloned()
.collect(),
});
println!(" Created complex Config with String, Vec, HashMap");
println!(" Wrapped in Arc for efficient sharing");
// Create several "workers" that need the config
let worker_listeners: Vec<_> = (0..10).map(|_| announcement.listener()).collect();
announcer.announce(Arc::clone(&config)).unwrap();
println!(" Broadcasted config to 10 workers");
println!(" Memory: 1 allocation, shared by all");
println!(" Time: ~100ns total (10 × 10ns Arc clone)\n");
// Workers can all access the config
for (i, listener) in worker_listeners.iter().enumerate() {
let worker_config = listener.try_listen().unwrap();
println!(" Worker {}: DB = {}", i, worker_config.database_url);
}
println!();
// SUMMARY
// =======
println!("=== Summary ===");
println!("✓ Understood why T: Clone can be expensive");
println!("✓ Used Arc<T> to avoid expensive clones");
println!("✓ Verified Arc reference counting");
println!("✓ Compared performance: Direct vs Arc");
println!("✓ Applied to real-world configuration broadcast");
println!("\nKEY TAKEAWAYS:");
println!(" 1. Announcement<T> clones T for each listener");
println!(" 2. Arc<T>: Clone is cheap (just refcount increment)");
println!(" 3. Use Arc<T> for types > 8 bytes with multiple listeners");
println!(" 4. Memory efficiency: 1 allocation shared by all");
println!(" 5. Time efficiency: ~5ns Arc clone vs ms for large clones");
println!("\nNEXT: See example 05 for blocking operations");
// EXPECTED OUTPUT:
// ================
// You should see:
// - Direct small data usage (efficient)
// - Direct large data usage (wasteful - 3MB allocated)
// - Arc large data usage (efficient - 1MB allocated)
// - Arc refcount verification showing shared memory
// - Performance comparison showing massive speedup
// - Real-world config broadcast example
}