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

+128
View File
@@ -0,0 +1,128 @@
//! Core operation benchmarks
//!
//! Measures the fundamental performance of basic announcement operations:
//! - Channel creation overhead
//! - Listener creation cost
//! - Announcement without listeners
//! - Try-listen operations (hit and miss cases)
//! - Status check operations
use announcement::Announcement;
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
/// Benchmark: Create a new announcement channel
fn bench_channel_creation(c: &mut Criterion) {
c.bench_function("core/channel_creation", |b| {
b.iter(|| {
let (_tx, _announcement) = Announcement::<i32>::new();
});
});
}
/// Benchmark: Create a listener from an existing announcement
fn bench_listener_creation(c: &mut Criterion) {
let (_tx, announcement) = Announcement::<i32>::new();
c.bench_function("core/listener_creation", |b| {
b.iter(|| black_box(announcement.listener()));
});
}
/// Benchmark: Announce with no listeners attached
fn bench_announce_no_listeners(c: &mut Criterion) {
c.bench_function("core/announce_no_listeners", |b| {
b.iter(|| {
let (tx, _) = Announcement::<i32>::new();
tx.announce(black_box(42)).unwrap();
});
});
}
/// Benchmark: try_listen when value is available (cache hit)
fn bench_try_listen_hit(c: &mut Criterion) {
let (tx, announcement) = Announcement::new();
tx.announce(42).unwrap();
let listener = announcement.listener();
c.bench_function("core/try_listen_hit", |b| {
b.iter(|| {
black_box(listener.try_listen());
});
});
}
/// Benchmark: try_listen when no value available (cache miss)
fn bench_try_listen_miss(c: &mut Criterion) {
let (_tx, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
c.bench_function("core/try_listen_miss", |b| {
b.iter(|| {
black_box(listener.try_listen());
});
});
}
/// Benchmark: Check is_announced status
fn bench_is_announced(c: &mut Criterion) {
let mut group = c.benchmark_group("core/is_announced");
// Not yet announced
let (_tx_not, announcement_not) = Announcement::<i32>::new();
group.bench_function("before_announce", |b| {
b.iter(|| {
black_box(announcement_not.is_announced());
});
});
// Already announced
let (tx, announcement_yes) = Announcement::<i32>::new();
let _result = tx.announce(42);
group.bench_function("after_announce", |b| {
b.iter(|| {
black_box(announcement_yes.is_announced());
});
});
group.finish();
}
/// Benchmark: Check is_closed status
fn bench_is_closed(c: &mut Criterion) {
let mut group = c.benchmark_group("core/is_closed");
// Not yet closed
let (_tx, announcement) = Announcement::<i32>::new();
group.bench_function("announcer_alive", |b| {
b.iter(|| {
black_box(announcement.is_closed());
});
});
// After dropping announcer
let announcement_closed = {
let (tx, announcement) = Announcement::<i32>::new();
drop(tx);
announcement
};
group.bench_function("announcer_dropped", |b| {
b.iter(|| {
black_box(announcement_closed.is_closed());
});
});
group.finish();
}
criterion_group!(
benches,
bench_channel_creation,
bench_listener_creation,
bench_announce_no_listeners,
bench_try_listen_hit,
bench_try_listen_miss,
bench_is_announced,
bench_is_closed,
);
criterion_main!(benches);
+564
View File
@@ -0,0 +1,564 @@
use announcement::Announcement;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::hint::black_box;
use std::sync::Arc;
use std::time::Duration;
// ============================================================================
// SHUTDOWN SIGNAL PATTERN - Notify all workers to shut down
// ============================================================================
fn bench_shutdown_signal(c: &mut Criterion) {
let mut group = c.benchmark_group("pattern/shutdown_signal");
// Simulate shutdown signal to various numbers of workers
for num_workers in [10, 100, 1_000, 10_000].iter() {
group.throughput(Throughput::Elements(*num_workers as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_workers),
num_workers,
|b, &n| {
b.iter(|| {
let (shutdown_tx, shutdown_announcement) = Announcement::new();
// Create N worker listeners
let mut workers = Vec::with_capacity(n);
for _ in 0..n {
workers.push(shutdown_announcement.listener());
}
// Send shutdown signal
shutdown_tx.announce(Arc::new(())).unwrap();
// All workers check for shutdown
for worker in &workers {
black_box(worker.try_listen());
}
});
},
);
}
group.finish();
}
fn bench_shutdown_with_tokio_tasks(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("pattern/shutdown_async");
group.sample_size(10);
group.measurement_time(Duration::from_secs(10));
for num_workers in [10, 100].iter() {
group.throughput(Throughput::Elements(*num_workers as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_workers),
num_workers,
|b, &n| {
b.to_async(&rt).iter(|| async move {
let (shutdown_tx, shutdown_announcement) = Announcement::new();
// Spawn N worker tasks
let mut handles = Vec::with_capacity(n);
for i in 0..n {
let listener = shutdown_announcement.listener();
handles.push(tokio::spawn(async move {
// Wait for shutdown
listener.listen().await.unwrap();
i
}));
}
// Small delay to let tasks start
tokio::time::sleep(Duration::from_millis(1)).await;
// Send shutdown signal
shutdown_tx.announce(Arc::new(())).unwrap();
// Wait for all workers to complete
for handle in handles {
black_box(handle.await.unwrap());
}
});
},
);
}
group.finish();
}
// ============================================================================
// CONFIG BROADCAST PATTERN - Share configuration to all services
// ============================================================================
#[derive(Clone, Debug)]
#[allow(dead_code)]
struct Config {
database_url: String,
api_key: String,
max_connections: usize,
timeout_ms: u64,
}
fn bench_config_broadcast(c: &mut Criterion) {
let mut group = c.benchmark_group("pattern/config_broadcast");
for num_services in [10, 100].iter() {
group.throughput(Throughput::Elements(*num_services as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_services),
num_services,
|b, &n| {
b.iter(|| {
let (config_tx, config_announcement) = Announcement::new();
// Create N service listeners
let mut services = Vec::with_capacity(n);
for _ in 0..n {
services.push(config_announcement.listener());
}
// Broadcast configuration
let config = Arc::new(Config {
database_url: "postgres://localhost/db".to_string(),
api_key: "secret-key-12345".to_string(),
max_connections: 100,
timeout_ms: 5000,
});
config_tx.announce(config).unwrap();
// All services receive config
for service in &services {
black_box(service.try_listen());
}
});
},
);
}
group.finish();
}
fn bench_config_broadcast_async(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("pattern/config_broadcast_async");
group.sample_size(10);
group.measurement_time(Duration::from_secs(10));
for num_services in [10, 100].iter() {
group.throughput(Throughput::Elements(*num_services as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_services),
num_services,
|b, &n| {
b.to_async(&rt).iter(|| async move {
let (config_tx, config_announcement) = Announcement::new();
// Spawn N service tasks waiting for config
let mut handles = Vec::with_capacity(n);
for i in 0..n {
let listener = config_announcement.listener();
handles.push(tokio::spawn(async move {
// Wait for config
let config: Arc<Config> = listener.listen().await.unwrap();
// Use config
(i, config.max_connections)
}));
}
// Small delay to let tasks start
tokio::time::sleep(Duration::from_millis(1)).await;
// Broadcast configuration
let config = Arc::new(Config {
database_url: "postgres://localhost/db".to_string(),
api_key: "secret-key-12345".to_string(),
max_connections: 100,
timeout_ms: 5000,
});
config_tx.announce(config).unwrap();
// Wait for all services to process config
for handle in handles {
black_box(handle.await.unwrap());
}
});
},
);
}
group.finish();
}
// ============================================================================
// EVENT FANOUT PATTERN - Broadcast events to many subscribers
// ============================================================================
#[derive(Clone, Debug)]
#[allow(dead_code)]
struct Event {
event_type: String,
timestamp: u64,
data: Vec<u8>,
}
fn bench_event_fanout(c: &mut Criterion) {
let mut group = c.benchmark_group("pattern/event_fanout");
for num_subscribers in [10, 100, 1_000].iter() {
group.throughput(Throughput::Elements(*num_subscribers as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_subscribers),
num_subscribers,
|b, &n| {
b.iter(|| {
let (event_tx, event_announcement) = Announcement::new();
// Create N subscriber listeners
let mut subscribers = Vec::with_capacity(n);
for _ in 0..n {
subscribers.push(event_announcement.listener());
}
// Broadcast event
let event = Arc::new(Event {
event_type: "user.created".to_string(),
timestamp: 1234567890,
data: vec![1, 2, 3, 4, 5],
});
event_tx.announce(event).unwrap();
// All subscribers receive event
for subscriber in &subscribers {
black_box(subscriber.try_listen());
}
});
},
);
}
group.finish();
}
fn bench_event_fanout_async(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("pattern/event_fanout_async");
group.sample_size(10);
group.measurement_time(Duration::from_secs(10));
for num_subscribers in [10, 100].iter() {
group.throughput(Throughput::Elements(*num_subscribers as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_subscribers),
num_subscribers,
|b, &n| {
b.to_async(&rt).iter(|| async move {
let (event_tx, event_announcement) = Announcement::new();
// Spawn N subscriber tasks
let mut handles = Vec::with_capacity(n);
for i in 0..n {
let listener = event_announcement.listener();
handles.push(tokio::spawn(async move {
let event: Arc<Event> = listener.listen().await.unwrap();
(i, event.timestamp)
}));
}
// Small delay
tokio::time::sleep(Duration::from_millis(1)).await;
// Broadcast event
let event = Arc::new(Event {
event_type: "user.created".to_string(),
timestamp: 1234567890,
data: vec![1, 2, 3, 4, 5],
});
event_tx.announce(event).unwrap();
// Wait for all subscribers
for handle in handles {
black_box(handle.await.unwrap());
}
});
},
);
}
group.finish();
}
// ============================================================================
// REQUEST-RESPONSE MULTIPLEXING - Multiple waiters for single response
// ============================================================================
#[derive(Clone, Debug)]
#[allow(dead_code)]
struct Response {
status: u16,
body: String,
}
fn bench_request_response_multiplex(c: &mut Criterion) {
let mut group = c.benchmark_group("pattern/request_response_multiplex");
// Multiple tasks waiting for same HTTP response
for num_waiters in [2, 5, 10, 50, 100].iter() {
group.throughput(Throughput::Elements(*num_waiters as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_waiters),
num_waiters,
|b, &n| {
b.iter(|| {
let (response_tx, response_announcement) = Announcement::new();
// Create N waiters for the same response
let mut waiters = Vec::with_capacity(n);
for _ in 0..n {
waiters.push(response_announcement.listener());
}
// Simulate response arrives
let response = Arc::new(Response {
status: 200,
body: "OK".to_string(),
});
response_tx.announce(response).unwrap();
// All waiters get response
for waiter in &waiters {
black_box(waiter.try_listen());
}
});
},
);
}
group.finish();
}
fn bench_request_response_multiplex_async(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("pattern/request_response_multiplex_async");
group.sample_size(10);
for num_waiters in [2, 5, 10, 50, 100].iter() {
group.throughput(Throughput::Elements(*num_waiters as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_waiters),
num_waiters,
|b, &n| {
b.to_async(&rt).iter(|| async move {
let (response_tx, response_announcement) = Announcement::new();
// Spawn N tasks waiting for response
let mut handles = Vec::with_capacity(n);
for i in 0..n {
let listener = response_announcement.listener();
handles.push(tokio::spawn(async move {
let response: Arc<Response> = listener.listen().await.unwrap();
(i, response.status)
}));
}
// Response arrives
let response = Arc::new(Response {
status: 200,
body: "OK".to_string(),
});
response_tx.announce(response).unwrap();
// Wait for all waiters
for handle in handles {
black_box(handle.await.unwrap());
}
});
},
);
}
group.finish();
}
// ============================================================================
// LAZY INITIALIZATION PATTERN - Signal when resource is ready
// ============================================================================
#[derive(Clone, Debug)]
#[allow(dead_code)]
struct Database {
connection_pool: Arc<Vec<String>>,
max_connections: usize,
}
fn bench_lazy_initialization(c: &mut Criterion) {
let mut group = c.benchmark_group("pattern/lazy_initialization");
for num_clients in [10, 100].iter() {
group.throughput(Throughput::Elements(*num_clients as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_clients),
num_clients,
|b, &n| {
b.iter(|| {
let (db_ready_tx, db_ready_announcement) = Announcement::new();
// N clients waiting for database to be ready
let mut clients = Vec::with_capacity(n);
for _ in 0..n {
clients.push(db_ready_announcement.listener());
}
// Simulate database initialization
let db = Arc::new(Database {
connection_pool: Arc::new(vec![
"conn1".to_string(),
"conn2".to_string(),
"conn3".to_string(),
]),
max_connections: 10,
});
// Signal ready
db_ready_tx.announce(db).unwrap();
// All clients can now proceed
for client in &clients {
black_box(client.try_listen());
}
});
},
);
}
group.finish();
}
fn bench_lazy_initialization_async(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("pattern/lazy_initialization_async");
group.sample_size(10);
for num_clients in [10, 100].iter() {
group.throughput(Throughput::Elements(*num_clients as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_clients),
num_clients,
|b, &n| {
b.to_async(&rt).iter(|| async move {
let (db_ready_tx, db_ready_announcement) = Announcement::new();
// Spawn N client tasks
let mut handles = Vec::with_capacity(n);
for i in 0..n {
let listener = db_ready_announcement.listener();
handles.push(tokio::spawn(async move {
// Wait for database
let db: Arc<Database> = listener.listen().await.unwrap();
(i, db.max_connections)
}));
}
let db = Arc::new(Database {
connection_pool: Arc::new(vec![
"conn1".to_string(),
"conn2".to_string(),
"conn3".to_string(),
]),
max_connections: 10,
});
db_ready_tx.announce(db).unwrap();
// Wait for all clients
for handle in handles {
black_box(handle.await.unwrap());
}
});
},
);
}
group.finish();
}
// ============================================================================
// BARRIER PATTERN - Multiple announcements that all must complete
// ============================================================================
fn bench_barrier_pattern(c: &mut Criterion) {
let mut group = c.benchmark_group("pattern/barrier");
// N tasks each announce completion, all wait for all to complete
for num_tasks in [5, 10, 25].iter() {
group.throughput(Throughput::Elements(*num_tasks as u64));
group.bench_with_input(
BenchmarkId::from_parameter(num_tasks),
num_tasks,
|b, &n| {
b.iter(|| {
let mut announcements = Vec::with_capacity(n);
let mut announcers = Vec::with_capacity(n);
// Create N announcement channels (one per task)
for _ in 0..n {
let (tx, announcement) = Announcement::<Arc<i32>>::new();
announcers.push(tx);
announcements.push(announcement);
}
// Each task listens to all other tasks
let mut all_listeners = Vec::with_capacity(n * n);
for announcement in &announcements {
for _ in 0..n {
all_listeners.push(announcement.listener());
}
}
// All tasks complete
for (i, announcer) in announcers.into_iter().enumerate() {
announcer.announce(Arc::new(i as i32)).unwrap();
}
// Verify all received
for listener in &all_listeners {
black_box(listener.try_listen());
}
});
},
);
}
group.finish();
}
criterion_group!(
shutdown,
bench_shutdown_signal,
bench_shutdown_with_tokio_tasks,
);
criterion_group!(config, bench_config_broadcast, bench_config_broadcast_async,);
criterion_group!(events, bench_event_fanout, bench_event_fanout_async,);
criterion_group!(
multiplex,
bench_request_response_multiplex,
bench_request_response_multiplex_async,
);
criterion_group!(
lazy_init,
bench_lazy_initialization,
bench_lazy_initialization_async,
);
criterion_group!(barrier, bench_barrier_pattern,);
criterion_main!(shutdown, config, events, multiplex, lazy_init, barrier);
+165
View File
@@ -0,0 +1,165 @@
//! Runtime comparison benchmarks
//!
//! Compares announcement performance across different async runtimes.
//! This verifies the runtime-agnostic design works consistently.
//!
//! Runtimes tested:
//! - tokio (most popular)
//! - smol (lightweight alternative)
//!
//! Expected result: Similar performance across runtimes since announcement
//! uses event-listener which abstracts runtime details.
#![allow(unused_must_use)]
use announcement::Announcement;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::hint::black_box;
use std::sync::Arc;
/// Benchmark: Basic announce and listen - tokio vs smol
fn bench_basic(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("runtimes/basic");
// Tokio version
group.bench_function("tokio", |b| {
b.to_async(&rt).iter(|| async {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let handle = tokio::spawn(async move { listener.listen().await });
announcer.announce(Arc::new(42)).unwrap();
black_box(handle.await.unwrap());
});
});
// Smol version
group.bench_function("smol", |b| {
b.iter(|| {
smol::block_on(async {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let handle = smol::spawn(async move { listener.listen().await });
announcer.announce(Arc::new(42)).unwrap();
black_box(handle.await);
});
});
});
group.finish();
}
/// Benchmark: Fanout to multiple listeners - tokio vs smol
fn bench_fanout(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("runtimes/fanout");
for n in [10, 100, 500].iter() {
group.throughput(Throughput::Elements(*n as u64));
// Tokio version
group.bench_with_input(BenchmarkId::new("tokio", n), n, |b, &n| {
b.to_async(&rt).iter(|| async move {
let (announcer, announcement) = Announcement::new();
let mut handles = Vec::with_capacity(n);
for _ in 0..n {
let listener = announcement.listener();
handles.push(tokio::spawn(async move { listener.listen().await }));
}
announcer.announce(Arc::new(42)).unwrap();
for h in handles {
black_box(h.await.unwrap());
}
});
});
// Smol version
group.bench_with_input(BenchmarkId::new("smol", n), n, |b, &n| {
b.iter(|| {
smol::block_on(async {
let (announcer, announcement) = Announcement::new();
let mut handles = Vec::with_capacity(n);
for _ in 0..n {
let listener = announcement.listener();
handles.push(smol::spawn(async move { listener.listen().await }));
}
announcer.announce(Arc::new(42)).unwrap();
for h in handles {
black_box(h.await);
}
});
});
});
}
group.finish();
}
/// Benchmark: Concurrent announcements - tokio vs smol
fn bench_concurrent(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("runtimes/concurrent");
// Tokio version
group.bench_function("tokio", |b| {
b.to_async(&rt).iter(|| async {
let mut handles = Vec::with_capacity(100);
for i in 0..100 {
handles.push(tokio::spawn(async move {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let recv_handle = tokio::spawn(async move { listener.listen().await });
announcer.announce(Arc::new(i)).unwrap();
recv_handle.await.unwrap()
}));
}
for h in handles {
black_box(h.await.unwrap());
}
});
});
// Smol version
group.bench_function("smol", |b| {
b.iter(|| {
smol::block_on(async {
let mut handles = Vec::with_capacity(100);
for i in 0..100 {
handles.push(smol::spawn(async move {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let recv_handle = smol::spawn(async move { listener.listen().await });
announcer.announce(Arc::new(i)).unwrap();
recv_handle.await
}));
}
for h in handles {
black_box(h.await);
}
});
});
});
group.finish();
}
criterion_group!(benches, bench_basic, bench_fanout, bench_concurrent,);
criterion_main!(benches);
+201
View File
@@ -0,0 +1,201 @@
//! Linearity and scaling benchmarks
//!
//! Proves O(N) linear scaling behavior for announcement operations:
//! - Announce operation: O(1) operation + O(N) listener wakeup
//! - Retrieval operation: O(1) per listener = O(N) total for N listeners
//! - Listener creation: O(1) per listener = O(N) total for N listeners
//!
//! Tests scales from 0 to 10 million listeners to verify linearity holds.
use announcement::Announcement;
use criterion::{
criterion_group, criterion_main, BenchmarkId, Criterion, PlotConfiguration, Throughput,
};
use std::hint::black_box;
use std::sync::Arc;
/// Benchmark: Linearity of announce operation with 0 to 10M listeners
///
/// This verifies that announcing scales linearly with listener count.
/// Expected: O(1) announce + O(N) wakeup = O(N) total
fn bench_linearity_announce(c: &mut Criterion) {
let mut group = c.benchmark_group("scaling/announce_linearity");
group.plot_config(PlotConfiguration::default().summary_scale(criterion::AxisScale::Linear));
// Test 3 logarithmically-spaced scales to prove O(N) linearity
// Mathematical proof: 3 points on log scale demonstrate linear behavior
let scales = [0, 1, 1_000, 100_000];
for n in scales.iter() {
group.throughput(Throughput::Elements(*n as u64));
group.bench_with_input(BenchmarkId::from_parameter(n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
// Create N listeners
let mut listeners = Vec::with_capacity(n);
for _ in 0..n {
listeners.push(announcement.listener());
}
// Measure announce time (should be O(1) + O(N) wakeup)
tx.announce(Arc::new(42)).unwrap();
// Keep listeners alive to measure full wakeup cost
black_box(listeners);
});
});
}
group.finish();
}
/// Benchmark: Linearity of retrieval operation with 1 to 10M listeners
///
/// This verifies that retrieving from N listeners scales linearly.
/// Expected: O(1) per listener × N listeners = O(N) total
fn bench_linearity_retrieval(c: &mut Criterion) {
let mut group = c.benchmark_group("scaling/retrieval_linearity");
group.plot_config(PlotConfiguration::default().summary_scale(criterion::AxisScale::Linear));
let scales = [1, 1_000, 100_000];
for n in scales.iter() {
group.throughput(Throughput::Elements(*n as u64));
group.bench_with_input(BenchmarkId::from_parameter(n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
// Create N listeners
let mut listeners = Vec::with_capacity(n);
for _ in 0..n {
listeners.push(announcement.listener());
}
// Announce first
tx.announce(Arc::new(42)).unwrap();
// Measure retrieval time (should be O(1) per listener = O(N) total)
for listener in &listeners {
black_box(listener.try_listen());
}
});
});
}
group.finish();
}
/// Benchmark: Linearity of listener creation with 1 to 10M listeners
///
/// This verifies that creating N listeners scales linearly.
/// Expected: O(1) per listener × N listeners = O(N) total
fn bench_linearity_listener_creation(c: &mut Criterion) {
let mut group = c.benchmark_group("scaling/listener_creation_linearity");
group.plot_config(PlotConfiguration::default().summary_scale(criterion::AxisScale::Linear));
let scales = [1, 1_000, 100_000];
for n in scales.iter() {
group.throughput(Throughput::Elements(*n as u64));
group.bench_with_input(BenchmarkId::from_parameter(n), n, |b, &n| {
b.iter(|| {
let (_tx, announcement) = Announcement::<Arc<i32>>::new();
// Create N listeners (should be O(N) with O(1) per listener)
let mut listeners = Vec::with_capacity(n);
for _ in 0..n {
listeners.push(announcement.listener());
}
black_box(listeners);
});
});
}
group.finish();
}
/// Benchmark: Zero listener overhead
///
/// Critical test: Announcing with 0 listeners MUST be nearly free (<5ns).
/// This is a key selling point of the design.
fn bench_zero_listener_overhead(c: &mut Criterion) {
let mut group = c.benchmark_group("scaling/zero_listener_overhead");
group.plot_config(PlotConfiguration::default().summary_scale(criterion::AxisScale::Linear));
// Compare 0 vs 1 vs 1000 listeners
// Zero listeners MUST be nearly free (<5ns)
let listener_counts = [0, 1, 1_000];
for n in listener_counts.iter() {
group.bench_with_input(BenchmarkId::from_parameter(n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
// Create N listeners
let mut listeners = Vec::with_capacity(n);
for _ in 0..n {
listeners.push(announcement.listener());
}
// Announce with N listeners
tx.announce(Arc::new(42)).unwrap();
black_box(listeners);
});
});
}
group.finish();
}
/// Benchmark: Overhead breakdown
///
/// Measures individual components to understand where time is spent.
fn bench_overhead_breakdown(c: &mut Criterion) {
let mut group = c.benchmark_group("scaling/overhead_breakdown");
// Measure individual components
group.bench_function("just_creation", |b| {
b.iter(|| {
let (_tx, _announcement) = Announcement::<Arc<i32>>::new();
});
});
group.bench_function("creation_plus_announce", |b| {
b.iter(|| {
let (tx, _announcement) = Announcement::new();
tx.announce(Arc::new(42)).unwrap();
});
});
group.bench_function("with_1_listener", |b| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let _listener = announcement.listener();
tx.announce(Arc::new(42)).unwrap();
});
});
group.bench_function("with_1_listener_retrieve", |b| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let listener = announcement.listener();
tx.announce(Arc::new(42)).unwrap();
black_box(listener.try_listen());
});
});
group.finish();
}
criterion_group!(
benches,
bench_linearity_announce,
bench_linearity_retrieval,
bench_linearity_listener_creation,
bench_zero_listener_overhead,
bench_overhead_breakdown,
);
criterion_main!(benches);
+231
View File
@@ -0,0 +1,231 @@
//! Stress test benchmarks for extreme scenarios
//!
//! Tests announcement under extreme conditions:
//! - Million-scale operations (1M+ listeners/announcements)
//! - Fully connected networks (N announcements × N listeners)
//! - Cascade patterns (chain reactions and trees)
use announcement::Announcement;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::hint::black_box;
use std::sync::Arc;
use std::time::Duration;
/// Benchmark: Million scale stress - 1M announcements × 10 listeners
fn bench_million_announcements(c: &mut Criterion) {
let mut group = c.benchmark_group("stress/million_scale");
group.sample_size(10);
group.measurement_time(Duration::from_secs(30));
group.bench_function("10K_announcements_x_10_listeners", |b| {
b.iter(|| {
let num_announcements = 10_000;
let listeners_per_announcement = 10;
let mut announcements = Vec::with_capacity(num_announcements);
let mut announcers = Vec::with_capacity(num_announcements);
// Create 10K announcement channels
for _ in 0..num_announcements {
let (tx, announcement) = Announcement::<Arc<i32>>::new();
announcers.push(tx);
announcements.push(announcement);
}
// Create 10 listeners for each announcement (100K total)
let mut all_listeners =
Vec::with_capacity(num_announcements * listeners_per_announcement);
for announcement in &announcements {
for _ in 0..listeners_per_announcement {
all_listeners.push(announcement.listener());
}
}
// Announce on all channels
for (i, announcer) in announcers.into_iter().enumerate() {
announcer.announce(Arc::new(i as i32)).unwrap();
}
// Verify delivery (sampling for performance)
for i in (0..all_listeners.len()).step_by(1000) {
black_box(all_listeners[i].try_listen());
}
});
});
group.finish();
}
/// Benchmark: Single announcement to 10K listeners
fn bench_one_to_million(c: &mut Criterion) {
let mut group = c.benchmark_group("stress/one_to_ten_thousand");
group.sample_size(10);
group.bench_function("1_announcement_x_10K_listeners", |b| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let num_listeners = 10_000;
let mut listeners = Vec::with_capacity(num_listeners);
for _ in 0..num_listeners {
listeners.push(announcement.listener());
}
tx.announce(Arc::new(42)).unwrap();
// Sample retrieval
for i in (0..listeners.len()).step_by(1000) {
black_box(listeners[i].try_listen());
}
});
});
group.finish();
}
/// Benchmark: Fully connected network - N announcements × N listeners
fn bench_fully_connected(c: &mut Criterion) {
let mut group = c.benchmark_group("stress/fully_connected");
group.sample_size(10);
group.measurement_time(Duration::from_secs(10));
// N announcements each received by N listeners = N² connections
for n in [10, 100, 500].iter() {
let total_connections = n * n;
group.throughput(Throughput::Elements(total_connections as u64));
group.bench_with_input(BenchmarkId::from_parameter(n), n, |b, &n| {
b.iter(|| {
// Create N announcement channels
let mut announcements = Vec::with_capacity(n);
let mut announcers = Vec::with_capacity(n);
for _ in 0..n {
let (tx, announcement) = Announcement::<Arc<i32>>::new();
announcers.push(tx);
announcements.push(announcement);
}
// Create N listeners for each announcement (N² total listeners)
let mut all_listeners = Vec::with_capacity(n * n);
for announcement in &announcements {
for _ in 0..n {
all_listeners.push(announcement.listener());
}
}
// Announce on all channels
for (i, announcer) in announcers.into_iter().enumerate() {
announcer.announce(Arc::new(i as i32)).unwrap();
}
// Retrieve all values
for listener in &all_listeners {
black_box(listener.try_listen());
}
});
});
}
group.finish();
}
/// Benchmark: Cascade chain - A triggers B triggers C...
fn bench_cascade_chain(c: &mut Criterion) {
let mut group = c.benchmark_group("stress/cascade_chain");
for chain_length in [10, 100, 500].iter() {
group.bench_with_input(
BenchmarkId::from_parameter(chain_length),
chain_length,
|b, &n| {
b.iter(|| {
let mut announcements = Vec::with_capacity(n);
let mut announcers = Vec::with_capacity(n);
// Create chain of announcements
for _ in 0..n {
let (tx, announcement) = Announcement::<Arc<i32>>::new();
announcers.push(tx);
announcements.push(announcement);
}
// Link chain: each announcement triggers next
let listeners: Vec<_> = announcements
.iter()
.take(n - 1)
.map(|a| a.listener())
.collect();
// Trigger cascade
for (i, announcer) in announcers.into_iter().enumerate() {
announcer.announce(Arc::new(i as i32)).unwrap();
}
// Verify propagation
for listener in &listeners {
black_box(listener.try_listen());
}
});
},
);
}
group.finish();
}
/// Benchmark: Cascade tree - A triggers B1, B2, B3... (tree fanout)
fn bench_cascade_tree(c: &mut Criterion) {
let mut group = c.benchmark_group("stress/cascade_tree");
for fanout in [2, 5, 10, 50].iter() {
group.throughput(Throughput::Elements(*fanout as u64));
group.bench_with_input(BenchmarkId::from_parameter(fanout), fanout, |b, &fanout| {
b.iter(|| {
// Root announcement
let (root_tx, root_announcement) = Announcement::<Arc<i32>>::new();
// Create fanout child announcements
let mut child_announcers = Vec::with_capacity(fanout);
let mut child_announcements = Vec::with_capacity(fanout);
for _ in 0..fanout {
let (tx, announcement) = Announcement::<Arc<i32>>::new();
child_announcers.push(tx);
child_announcements.push(announcement);
}
// Root listener triggers all children
let _root_listener = root_announcement.listener();
// Create listeners for all children
let mut child_listeners = Vec::with_capacity(fanout);
for announcement in &child_announcements {
child_listeners.push(announcement.listener());
}
// Trigger cascade
root_tx.announce(Arc::new(0)).unwrap();
for (i, announcer) in child_announcers.into_iter().enumerate() {
announcer.announce(Arc::new(i as i32)).unwrap();
}
// Verify all children received
for listener in &child_listeners {
black_box(listener.try_listen());
}
});
});
}
group.finish();
}
criterion_group!(
benches,
bench_million_announcements,
bench_one_to_million,
bench_fully_connected,
bench_cascade_chain,
bench_cascade_tree,
);
criterion_main!(benches);
+284
View File
@@ -0,0 +1,284 @@
use announcement::Announcement;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::collections::HashMap;
use std::hint::black_box;
use std::sync::Arc;
// Small type: 4 bytes
fn bench_small_type(c: &mut Criterion) {
let mut group = c.benchmark_group("small_type_i32");
for n in [1, 10, 100, 500].iter() {
group.throughput(Throughput::Elements(*n as u64));
// Direct Clone
group.bench_with_input(BenchmarkId::new("direct_clone", n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let mut listeners = Vec::new();
for _ in 0..n {
listeners.push(announcement.listener());
}
tx.announce(42i32).unwrap();
for listener in listeners {
black_box(listener.try_listen());
}
});
});
// Arc<i32> Clone
group.bench_with_input(BenchmarkId::new("arc_clone", n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let mut listeners = Vec::new();
for _ in 0..n {
listeners.push(announcement.listener());
}
tx.announce(Arc::new(42i32)).unwrap();
for listener in listeners {
black_box(listener.try_listen());
}
});
});
}
group.finish();
}
// Medium type: 1KB
fn bench_medium_type(c: &mut Criterion) {
let mut group = c.benchmark_group("medium_type_1kb");
for n in [1, 10, 100, 500].iter() {
group.throughput(Throughput::Elements(*n as u64));
// Direct Clone
group.bench_with_input(BenchmarkId::new("direct_clone", n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let mut listeners = Vec::new();
for _ in 0..n {
listeners.push(announcement.listener());
}
tx.announce([0u8; 1024]).unwrap();
for listener in listeners {
black_box(listener.try_listen());
}
});
});
// Arc Clone
group.bench_with_input(BenchmarkId::new("arc_clone", n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let mut listeners = Vec::new();
for _ in 0..n {
listeners.push(announcement.listener());
}
tx.announce(Arc::new([0u8; 1024])).unwrap();
for listener in listeners {
black_box(listener.try_listen());
}
});
});
}
group.finish();
}
// Large type: 1MB
fn bench_large_type(c: &mut Criterion) {
let mut group = c.benchmark_group("large_type_1mb");
group.sample_size(10); // Reduce sample size for large data
for n in [1, 10, 100, 500].iter() {
group.throughput(Throughput::Elements(*n as u64));
// Direct Clone
group.bench_with_input(BenchmarkId::new("direct_clone", n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let mut listeners = Vec::new();
for _ in 0..n {
listeners.push(announcement.listener());
}
tx.announce(vec![0u8; 1024 * 1024]).unwrap();
for listener in listeners {
black_box(listener.try_listen());
}
});
});
// Arc Clone
group.bench_with_input(BenchmarkId::new("arc_clone", n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let mut listeners = Vec::new();
for _ in 0..n {
listeners.push(announcement.listener());
}
tx.announce(Arc::new(vec![0u8; 1024 * 1024])).unwrap();
for listener in listeners {
black_box(listener.try_listen());
}
});
});
}
group.finish();
}
// Complex type: Config with String, Vec, HashMap
#[derive(Clone, Debug)]
#[allow(dead_code)]
struct Config {
name: String,
tags: Vec<String>,
settings: HashMap<String, String>,
data: Vec<u8>,
}
impl Config {
fn new() -> Self {
let mut settings = HashMap::new();
settings.insert("key1".to_string(), "value1".to_string());
settings.insert("key2".to_string(), "value2".to_string());
settings.insert("key3".to_string(), "value3".to_string());
Config {
name: "MyConfig".to_string(),
tags: vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()],
settings,
data: vec![0u8; 1024],
}
}
}
fn bench_complex_type(c: &mut Criterion) {
let mut group = c.benchmark_group("complex_type_config");
for n in [1, 10, 100, 500].iter() {
group.throughput(Throughput::Elements(*n as u64));
// Direct Clone
group.bench_with_input(BenchmarkId::new("direct_clone", n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let mut listeners = Vec::new();
for _ in 0..n {
listeners.push(announcement.listener());
}
tx.announce(Config::new()).unwrap();
for listener in listeners {
black_box(listener.try_listen());
}
});
});
// Arc Clone
group.bench_with_input(BenchmarkId::new("arc_clone", n), n, |b, &n| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let mut listeners = Vec::new();
for _ in 0..n {
listeners.push(announcement.listener());
}
tx.announce(Arc::new(Config::new())).unwrap();
for listener in listeners {
black_box(listener.try_listen());
}
});
});
}
group.finish();
}
// Comparison benchmark showing the crossover point
fn bench_crossover_analysis(c: &mut Criterion) {
let mut group = c.benchmark_group("crossover_analysis");
// Test various type sizes to find where Arc becomes beneficial
let sizes = [4, 8, 16, 32, 64, 128, 256, 512];
for size in sizes.iter() {
group.throughput(Throughput::Bytes(*size as u64));
// Direct clone with 10 listeners
group.bench_with_input(
BenchmarkId::new("direct_10_listeners", size),
size,
|b, &size| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let mut listeners = Vec::new();
for _ in 0..10 {
listeners.push(announcement.listener());
}
let data = vec![0u8; size];
tx.announce(data).unwrap();
for listener in listeners {
black_box(listener.try_listen());
}
});
},
);
// Arc clone with 10 listeners
group.bench_with_input(
BenchmarkId::new("arc_10_listeners", size),
size,
|b, &size| {
b.iter(|| {
let (tx, announcement) = Announcement::new();
let mut listeners = Vec::new();
for _ in 0..10 {
listeners.push(announcement.listener());
}
let data = Arc::new(vec![0u8; size]);
tx.announce(data).unwrap();
for listener in listeners {
black_box(listener.try_listen());
}
});
},
);
}
group.finish();
}
criterion_group!(
benches,
bench_small_type,
bench_medium_type,
bench_large_type,
bench_complex_type,
bench_crossover_analysis,
);
criterion_main!(benches);
+237
View File
@@ -0,0 +1,237 @@
//! Comparison benchmarks against alternative implementations
//!
//! Compares announcement's oneshot broadcast pattern against:
//! - tokio::sync::oneshot (N separate channels)
//! - tokio::sync::broadcast (multi-shot channel used for oneshot)
//! - async-broadcast (runtime-agnostic broadcast)
//! - oneshot_broadcast crate (direct competitor)
//!
//! Each comparison uses the oneshot pattern: send once, N receivers.
#![allow(deprecated)]
#![allow(unused_must_use)]
use announcement::Announcement;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::hint::black_box;
use std::sync::Arc;
/// Compare: announcement vs tokio::sync::oneshot (N separate channels)
///
/// Pattern: One announcement to N listeners vs N separate oneshot channels
fn bench_vs_tokio_oneshot(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("vs_alternatives/tokio_oneshot");
for n in [5, 10, 50, 100].iter() {
group.throughput(Throughput::Elements(*n as u64));
// announcement - O(1) channel, O(N) arc clones
group.bench_with_input(BenchmarkId::new("announcement", n), n, |b, &n| {
b.to_async(&rt).iter(|| async move {
let (tx, announcement) = Announcement::new();
let mut handles = Vec::with_capacity(n);
for _ in 0..n {
let listener = announcement.listener();
handles.push(tokio::spawn(async move { listener.listen().await }));
}
tx.announce(Arc::new(42)).unwrap();
for h in handles {
black_box(h.await.unwrap());
}
});
});
// N oneshots - O(N) channels, O(N) sends
group.bench_with_input(BenchmarkId::new("tokio_oneshot", n), n, |b, &n| {
b.to_async(&rt).iter(|| async move {
let mut senders = Vec::with_capacity(n);
let mut handles = Vec::with_capacity(n);
for _ in 0..n {
let (tx, rx) = tokio::sync::oneshot::channel();
senders.push(tx);
handles.push(tokio::spawn(async move { rx.await.unwrap() }));
}
let data = Arc::new(42);
for tx in senders {
tx.send(Arc::clone(&data)).unwrap();
}
for h in handles {
black_box(h.await.unwrap());
}
});
});
}
group.finish();
}
/// Compare: announcement vs tokio::sync::broadcast (for oneshot pattern)
///
/// tokio::broadcast is designed for multi-shot but can be used for oneshot
fn bench_vs_tokio_broadcast(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("vs_alternatives/tokio_broadcast");
for n in [1, 10, 100, 500].iter() {
group.throughput(Throughput::Elements(*n as u64));
// announcement - optimized for oneshot
group.bench_with_input(BenchmarkId::new("announcement", n), n, |b, &n| {
b.to_async(&rt).iter(|| async move {
let (announcer, announcement) = Announcement::new();
let mut handles = Vec::with_capacity(n);
for _ in 0..n {
let listener = announcement.listener();
handles.push(tokio::spawn(async move { listener.listen().await }));
}
announcer.announce(Arc::new(42)).unwrap();
for h in handles {
black_box(h.await.unwrap());
}
});
});
// tokio::broadcast - designed for multi-shot
group.bench_with_input(BenchmarkId::new("tokio_broadcast", n), n, |b, &n| {
b.to_async(&rt).iter(|| async move {
let (tx, _) = tokio::sync::broadcast::channel::<Arc<i32>>(1);
let mut handles = Vec::with_capacity(n);
for _ in 0..n {
let mut rx = tx.subscribe();
handles.push(tokio::spawn(async move { rx.recv().await.unwrap() }));
}
tx.send(Arc::new(42)).unwrap();
drop(tx);
for h in handles {
black_box(h.await.unwrap());
}
});
});
}
group.finish();
}
/// Compare: announcement vs async-broadcast (runtime-agnostic)
///
/// NOTE: async-broadcast benchmark is commented out due to a bug in the crate
/// where broadcast() fails with SendError even with receivers waiting.
/// See tests/async_broadcast_race_repro.rs for minimal reproduction.
fn bench_vs_async_broadcast(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("vs_alternatives/async_broadcast");
for n in [1, 10, 100, 500].iter() {
group.throughput(Throughput::Elements(*n as u64));
// announcement
group.bench_with_input(BenchmarkId::new("announcement", n), n, |b, &n| {
b.to_async(&rt).iter(|| async move {
let (announcer, announcement) = Announcement::new();
let mut handles = Vec::with_capacity(n);
for _ in 0..n {
let listener = announcement.listener();
handles.push(tokio::spawn(async move { listener.listen().await }));
}
announcer.announce(Arc::new(42)).unwrap();
for h in handles {
black_box(h.await.unwrap());
}
});
});
// async-broadcast - COMMENTED OUT due to crate bug
// Bug: broadcast() randomly fails with SendError even when receivers exist
// Reproduction: tests/async_broadcast_race_repro.rs
/*
group.bench_with_input(BenchmarkId::new("async_broadcast", n), n, |b, &n| {
b.to_async(&rt).iter(|| async move {
let (tx, _) = async_broadcast::broadcast::<Arc<i32>>(1);
let mut handles = Vec::with_capacity(n);
for _ in 0..n {
let mut rx = tx.new_receiver();
handles.push(tokio::spawn(async move { rx.recv().await.unwrap() }));
}
tx.broadcast(Arc::new(42)).await.unwrap();
for h in handles {
black_box(h.await.unwrap());
}
});
});
*/
}
group.finish();
}
/// Show where tokio::broadcast excels: multiple announcements
///
/// This is NOT what announcement is designed for - this shows the tradeoffs
fn bench_multishot_pattern(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("vs_alternatives/multishot_pattern");
// announcement - must create new channel each time
group.bench_function("announcement_recreate", |b| {
b.to_async(&rt).iter(|| async move {
for i in 0..10 {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let handle = tokio::spawn(async move { listener.listen().await });
announcer.announce(Arc::new(i)).unwrap();
black_box(handle.await.unwrap());
}
});
});
// tokio::broadcast - designed for this
group.bench_function("tokio_broadcast_multishot", |b| {
b.to_async(&rt).iter(|| async move {
let (tx, _) = tokio::sync::broadcast::channel::<Arc<i32>>(10);
let mut rx = tx.subscribe();
let handle = tokio::spawn(async move {
for _ in 0..10 {
black_box(rx.recv().await.unwrap());
}
});
for i in 0..10 {
tx.send(Arc::new(i)).unwrap();
}
handle.await.unwrap();
});
});
group.finish();
}
criterion_group!(
benches,
bench_vs_tokio_oneshot,
bench_vs_tokio_broadcast,
bench_vs_async_broadcast,
bench_multishot_pattern,
);
criterion_main!(benches);