From e700444169f8add69698d12c54833435654d89f6 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 18:58:05 +0800 Subject: [PATCH 01/26] refactor(mpsc): simplify unbounded channel coordination Signed-off-by: tison --- asyncband/src/mpsc/unbounded/buffer.rs | 15 +- asyncband/src/mpsc/unbounded/buffer_tests.rs | 29 -- asyncband/src/mpsc/unbounded/mod.rs | 274 +++---------------- asyncband/src/mpsc/unbounded/receiver.rs | 171 ++++++++++++ asyncband/src/mpsc/unbounded/sender.rs | 88 ++++++ tests-integration/tests/mpsc_test/main.rs | 4 +- 6 files changed, 297 insertions(+), 284 deletions(-) create mode 100644 asyncband/src/mpsc/unbounded/receiver.rs create mode 100644 asyncband/src/mpsc/unbounded/sender.rs diff --git a/asyncband/src/mpsc/unbounded/buffer.rs b/asyncband/src/mpsc/unbounded/buffer.rs index 8df2640..29fbd54 100644 --- a/asyncband/src/mpsc/unbounded/buffer.rs +++ b/asyncband/src/mpsc/unbounded/buffer.rs @@ -25,7 +25,6 @@ pub const SEGMENT_BYTES: usize = 32 * 1024; pub struct Buffer { writable: VecDeque, sealed: VecDeque>, - spare: VecDeque, } impl Buffer { @@ -33,7 +32,6 @@ impl Buffer { Self { writable: VecDeque::new(), sealed: VecDeque::new(), - spare: VecDeque::new(), } } @@ -48,11 +46,7 @@ impl Buffer { pub fn push(&mut self, value: T) { if self.writable.len() == Self::segment_capacity() { - let next = if self.spare.capacity() == 0 { - VecDeque::with_capacity(Self::segment_capacity()) - } else { - mem::take(&mut self.spare) - }; + let next = VecDeque::with_capacity(Self::segment_capacity()); let sealed = mem::replace(&mut self.writable, next); self.sealed.push_back(sealed); } @@ -62,14 +56,11 @@ impl Buffer { pub fn refill(&mut self, batch: &mut VecDeque) { debug_assert!(batch.is_empty()); if let Some(sealed) = self.sealed.pop_front() { - // Keep one empty segment for the next producer rollover. Every other consumed - // segment is released, so retained payload storage does not track peak occupancy. - self.spare = mem::replace(batch, sealed); + *batch = sealed; if self.sealed.is_empty() && self.sealed.capacity() * size_of::>() > 1024 { self.sealed = VecDeque::new(); } } else if !self.writable.is_empty() { - self.spare = VecDeque::new(); mem::swap(batch, &mut self.writable); } } @@ -77,7 +68,7 @@ impl Buffer { pub fn pop_batch(batch: &mut VecDeque) -> T { if batch.len() == 1 && batch.capacity().saturating_mul(size_of::()) > SEGMENT_BYTES { - // Retire the allocation on the last value, outside the inbox lock. Keep this as a tail + // Retire the allocation on the last value, outside the shared lock. Keep this as a tail // expression to avoid intermediate storage for large inline values. mem::take(batch).pop_front() } else { diff --git a/asyncband/src/mpsc/unbounded/buffer_tests.rs b/asyncband/src/mpsc/unbounded/buffer_tests.rs index de40b1e..8be63c3 100644 --- a/asyncband/src/mpsc/unbounded/buffer_tests.rs +++ b/asyncband/src/mpsc/unbounded/buffer_tests.rs @@ -24,7 +24,6 @@ use super::pop_batch; fn allocated_bytes(buffer: &Buffer, batch: &VecDeque) -> usize { let slots = batch.capacity() + buffer.writable.capacity() - + buffer.spare.capacity() + buffer.sealed.iter().map(VecDeque::capacity).sum::(); slots * size_of::() } @@ -36,20 +35,6 @@ fn receive(buffer: &mut Buffer, batch: &mut VecDeque) -> T { pop_batch(batch) } -#[test] -fn messages_arriving_during_a_batch_remain_in_fifo_order() { - let mut buffer = Buffer::new(); - let mut batch = VecDeque::new(); - buffer.push(1); - buffer.push(2); - assert_eq!(receive(&mut buffer, &mut batch), 1); - buffer.push(3); - assert_eq!(receive(&mut buffer, &mut batch), 2); - assert_eq!(receive(&mut buffer, &mut batch), 3); - buffer.refill(&mut batch); - assert!(batch.is_empty()); -} - #[test] fn a_partial_drain_reclaims_segments_and_preserves_new_sends() { let mut buffer = Buffer::new(); @@ -98,17 +83,3 @@ fn oversized_inline_values_release_the_allocation_on_the_last_receive() { assert_eq!(receive(&mut buffer, &mut batch), [7u8; SEGMENT_BYTES + 1]); assert_eq!(allocated_bytes(&buffer, &batch), 0); } - -#[test] -fn zero_sized_values_do_not_require_segments() { - let mut buffer = Buffer::new(); - let mut batch = VecDeque::new(); - for _ in 0..32 { - buffer.push(()); - } - for _ in 0..32 { - receive(&mut buffer, &mut batch); - } - buffer.refill(&mut batch); - assert!(batch.is_empty()); -} diff --git a/asyncband/src/mpsc/unbounded/mod.rs b/asyncband/src/mpsc/unbounded/mod.rs index 7c69ac4..be2b087 100644 --- a/asyncband/src/mpsc/unbounded/mod.rs +++ b/asyncband/src/mpsc/unbounded/mod.rs @@ -18,263 +18,53 @@ //! An unbounded multi-producer, single-consumer queue for sending values between asynchronous //! tasks. -use std::collections::VecDeque; -use std::fmt; -use std::future::poll_fn; -use std::mem; use std::sync::Arc; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; -use std::task::Context; -use std::task::Poll; use std::task::Waker; use self::buffer::Buffer; -use self::buffer::pop_batch; -use super::RecvError; -use super::SendError; -use super::TryRecvError; use crate::internal::mutex::Mutex; -// Buffer owns segmentation and reclamation; Inbox serializes enqueueing, registration, and close. mod buffer; +mod receiver; +mod sender; + +pub use self::receiver::UnboundedReceiver; +pub use self::sender::UnboundedSender; /// Creates an unbounded mpsc channel whose send operation never waits for capacity. /// -/// While the receiver is alive, each send appends its value immediately. Pending messages can -/// therefore grow with producer demand and are limited only by successful memory allocation. Use a -/// bounded channel or external admission control when producers may outpace the receiver. +/// Pending messages can grow with producer demand and are limited only by successful memory +/// allocation. Use a [`bounded`](crate::mpsc::bounded) channel or external admission control when +/// producers may outpace the receiver. +/// +/// Messages are received in the order they were sent. After the last sender is dropped, the +/// receiver drains queued messages before reporting disconnection. /// /// Storage is reclaimed incrementally as messages are received. A bounded amount of empty /// storage may be retained for reuse, independently of the channel's previous peak occupancy. +/// +/// Operations briefly acquire an internal mutex; no lock is held across an await point or while +/// invoking waker callbacks or message destructors. Sending and trying to receive may wait to +/// acquire this mutex, but never wait for capacity or new messages. pub fn unbounded() -> (UnboundedSender, UnboundedReceiver) { - let state = Arc::new(Shared { - senders: AtomicUsize::new(1), - inbox: Mutex::new(Inbox { - buffer: Buffer::new(), - receiver_alive: true, - rx_waker: None, - }), - }); - let sender = UnboundedSender { - state: state.clone(), - }; - let receiver = UnboundedReceiver { - state, - batch: Mutex::new(VecDeque::new()), - }; - (sender, receiver) + let shared = Arc::new(Mutex::new(State { + buffer: Buffer::new(), + senders: 1, + receiver: true, + recv_waker: None, + })); + ( + UnboundedSender::new(shared.clone()), + UnboundedReceiver::new(shared), + ) } -struct Shared { - // Endpoint cloning and ordinary drops do not contend with message traffic. - senders: AtomicUsize, - inbox: Mutex>, -} - -// Queue contents, receiver liveness, and its wake registration share one lock. Registering a -// wait and checking its condition cannot race with sending or receiver disconnection. -struct Inbox { +// Queue contents, endpoint liveness, and wake registration share one lock. Only the receiver +// accesses its current batch; refilling that batch preserves the order of concurrent sends. +struct State { buffer: Buffer, - receiver_alive: bool, - rx_waker: Option, -} - -/// The sending endpoint of an unbounded mpsc channel. -/// -/// Instances are created by the [`unbounded`] function. -pub struct UnboundedSender { - state: Arc>, -} - -impl Clone for UnboundedSender { - fn clone(&self) -> Self { - self.state.senders.fetch_add(1, Ordering::Release); - Self { - state: self.state.clone(), - } - } -} - -impl fmt::Debug for UnboundedSender { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("UnboundedSender").finish_non_exhaustive() - } -} - -impl Drop for UnboundedSender { - fn drop(&mut self) { - if self.state.senders.fetch_sub(1, Ordering::AcqRel) == 1 { - let waker = self.state.inbox.lock().rx_waker.take(); - if let Some(waker) = waker { - waker.wake(); - } - } - } + senders: usize, + // True while the receiving endpoint is alive. + receiver: bool, + recv_waker: Option, } - -impl UnboundedSender { - /// Enqueues a message without waiting for capacity. - /// - /// This operation is synchronous because the channel has no capacity limit. If the receiver has - /// been dropped, the returned error contains `value`. - pub fn send(&self, value: T) -> Result<(), SendError> { - let waker = { - let mut state = self.state.inbox.lock(); - if !state.receiver_alive { - return Err(SendError::new(value)); - } - state.buffer.push(value); - state.rx_waker.take() - }; - if let Some(waker) = waker { - waker.wake(); - } - Ok(()) - } -} - -/// The receiving endpoint of an unbounded mpsc channel. -/// -/// Instances are created by the [`unbounded`] function. -pub struct UnboundedReceiver { - state: Arc>, - // Only accessed through `get_mut`; the mutex preserves Sync for Send-only payloads. - batch: Mutex>, -} - -impl fmt::Debug for UnboundedReceiver { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("UnboundedReceiver").finish_non_exhaustive() - } -} - -impl Drop for UnboundedReceiver { - fn drop(&mut self) { - let batch = mem::take(self.batch.get_mut()); - let (shared, waker) = { - let mut state = self.state.inbox.lock(); - state.receiver_alive = false; - ( - mem::replace(&mut state.buffer, Buffer::new()), - state.rx_waker.take(), - ) - }; - // Destructors may send again. A waker may also own a sender and form an ownership cycle. - drop((batch, shared, waker)); - } -} - -impl UnboundedReceiver { - /// Attempts to receive the next queued value without waiting. - /// - /// An empty channel returns [`TryRecvError::Empty`] while at least one sender remains, or - /// [`TryRecvError::Disconnected`] after every sender has been dropped and all queued values - /// have been consumed. - /// - /// # Examples - /// - /// ``` - /// use asyncband::mpsc::TryRecvError; - /// use asyncband::mpsc::unbounded; - /// - /// let (tx, mut rx) = unbounded(); - /// tx.send("first").unwrap(); - /// tx.send("second").unwrap(); - /// - /// assert_eq!(rx.try_recv(), Ok("first")); - /// assert_eq!(rx.try_recv(), Ok("second")); - /// assert_eq!(rx.try_recv(), Err(TryRecvError::Empty)); - /// drop(tx); - /// assert_eq!(rx.try_recv(), Err(TryRecvError::Disconnected)); - /// ``` - pub fn try_recv(&mut self) -> Result { - let batch = self.batch.get_mut(); - if batch.is_empty() { - let mut state = self.state.inbox.lock(); - state.buffer.refill(batch); - if batch.is_empty() { - // Holding the inbox lock excludes a final send between the empty observation and - // the sender-count check; disconnection needs no second queue read. - return Err(if self.state.senders.load(Ordering::Acquire) == 0 { - TryRecvError::Disconnected - } else { - TryRecvError::Empty - }); - } - } - Ok(pop_batch(batch)) - } - - /// Waits for and receives the next value. - /// - /// If no value is queued, this method waits until a sender adds one or the last sender is - /// dropped. It returns [`RecvError::Disconnected`] only after all senders are gone and the - /// queue has been drained. - /// - /// # Cancel safety - /// - /// Dropping a pending `recv` does not remove a message from the channel. A later receive - /// operation can still observe the next queued value, so `recv` may safely be raced with other - /// futures in a selection construct. - /// - /// # Examples - /// - /// ``` - /// # #[tokio::main] - /// # async fn main() { - /// use asyncband::mpsc; - /// let (tx, mut rx) = mpsc::unbounded(); - /// - /// tx.send("first").unwrap(); - /// tx.send("second").unwrap(); - /// drop(tx); - /// - /// assert_eq!(rx.recv().await, Ok("first")); - /// assert_eq!(rx.recv().await, Ok("second")); - /// assert_eq!(rx.recv().await, Err(mpsc::RecvError::Disconnected)); - /// # } - /// ``` - pub async fn recv(&mut self) -> Result { - poll_fn(|cx| self.poll_recv(cx)).await - } - - fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll> { - let batch = self.batch.get_mut(); - if !batch.is_empty() { - return Poll::Ready(Ok(pop_batch(batch))); - } - // Waker clone/drop callbacks can reenter this channel. Clone outside the lock, then - // recheck the condition before registering; keep replaced wakers outside the lock too. - let mut new_waker = None; - loop { - let mut state = self.state.inbox.lock(); - state.buffer.refill(batch); - if !batch.is_empty() { - drop(state); - return Poll::Ready(Ok(pop_batch(batch))); - } - if self.state.senders.load(Ordering::Acquire) == 0 { - return Poll::Ready(Err(RecvError::Disconnected)); - } - if state - .rx_waker - .as_ref() - .is_some_and(|waker| waker.will_wake(cx.waker())) - { - return Poll::Pending; - } - if let Some(waker) = new_waker.take() { - let old_waker = state.rx_waker.replace(waker); - drop(state); - drop(old_waker); - return Poll::Pending; - } - drop(state); - new_waker = Some(cx.waker().clone()); - } - } -} - -// No operation relies on a pinned location for the receiver batch or its values. -impl Unpin for UnboundedReceiver {} diff --git a/asyncband/src/mpsc/unbounded/receiver.rs b/asyncband/src/mpsc/unbounded/receiver.rs new file mode 100644 index 0000000..4b3ea76 --- /dev/null +++ b/asyncband/src/mpsc/unbounded/receiver.rs @@ -0,0 +1,171 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::collections::VecDeque; +use std::fmt; +use std::future::poll_fn; +use std::mem; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; + +use super::State; +use super::buffer::Buffer; +use super::buffer::pop_batch; +use crate::internal::mutex::Mutex; +use crate::mpsc::RecvError; +use crate::mpsc::TryRecvError; + +/// The receiving endpoint of an unbounded mpsc channel. +/// +/// Instances are created by the [`unbounded`](crate::mpsc::unbounded) function. Dropping the +/// receiver discards queued values and makes subsequent sends fail. +pub struct UnboundedReceiver { + shared: Arc>>, + // Only accessed through `get_mut`; the mutex preserves Sync for Send-only payloads. + batch: Mutex>, +} + +impl fmt::Debug for UnboundedReceiver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UnboundedReceiver").finish_non_exhaustive() + } +} + +impl Drop for UnboundedReceiver { + fn drop(&mut self) { + let batch = mem::take(self.batch.get_mut()); + let (buffer, waker) = { + let mut state = self.shared.lock(); + state.receiver = false; + ( + mem::replace(&mut state.buffer, Buffer::new()), + state.recv_waker.take(), + ) + }; + // Destructors may send again. A waker may also own a sender and form an ownership cycle. + drop((batch, buffer, waker)); + } +} + +impl UnboundedReceiver { + pub(super) fn new(shared: Arc>>) -> Self { + Self { + shared, + batch: Mutex::new(VecDeque::new()), + } + } + + /// Attempts to receive the next queued value without waiting for a new message. + /// + /// An empty channel returns [`TryRecvError::Empty`] while at least one sender remains, or + /// [`TryRecvError::Disconnected`] after every sender has been dropped and all queued values + /// have been consumed. + /// + /// # Examples + /// + /// ``` + /// use asyncband::mpsc::TryRecvError; + /// use asyncband::mpsc::unbounded; + /// + /// let (tx, mut rx) = unbounded(); + /// tx.send("first").unwrap(); + /// tx.send("second").unwrap(); + /// + /// assert_eq!(rx.try_recv(), Ok("first")); + /// assert_eq!(rx.try_recv(), Ok("second")); + /// assert_eq!(rx.try_recv(), Err(TryRecvError::Empty)); + /// drop(tx); + /// assert_eq!(rx.try_recv(), Err(TryRecvError::Disconnected)); + /// ``` + pub fn try_recv(&mut self) -> Result { + let batch = self.batch.get_mut(); + if batch.is_empty() { + let mut state = self.shared.lock(); + state.buffer.refill(batch); + if batch.is_empty() { + return Err(if state.senders == 0 { + TryRecvError::Disconnected + } else { + TryRecvError::Empty + }); + } + } + Ok(pop_batch(batch)) + } + + /// Waits for and receives the next value. + /// + /// If no value is queued, this method waits until a sender adds one or the last sender is + /// dropped. It returns [`RecvError::Disconnected`] only after all senders are gone and the + /// queue has been drained. + /// + /// # Cancel safety + /// + /// Dropping a pending `recv` does not remove a message from the channel. A later receive + /// operation can still observe the next queued value, so `recv` may safely be raced with other + /// futures in a selection construct. + /// + /// # Examples + /// + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// use asyncband::mpsc; + /// let (tx, mut rx) = mpsc::unbounded(); + /// + /// tx.send("first").unwrap(); + /// tx.send("second").unwrap(); + /// drop(tx); + /// + /// assert_eq!(rx.recv().await, Ok("first")); + /// assert_eq!(rx.recv().await, Ok("second")); + /// assert_eq!(rx.recv().await, Err(mpsc::RecvError::Disconnected)); + /// # } + /// ``` + pub async fn recv(&mut self) -> Result { + poll_fn(|cx| self.poll_recv(cx)).await + } + + fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll> { + let batch = self.batch.get_mut(); + if !batch.is_empty() { + return Poll::Ready(Ok(pop_batch(batch))); + } + // Waker clone/drop callbacks can send into this channel, so run them outside the lock. + let waker = cx.waker().clone(); + let mut state = self.shared.lock(); + state.buffer.refill(batch); + if !batch.is_empty() { + drop(state); + return Poll::Ready(Ok(pop_batch(batch))); + } + if state.senders == 0 { + let old = state.recv_waker.take(); + drop(state); + drop(old); + return Poll::Ready(Err(RecvError::Disconnected)); + } + let old = state.recv_waker.replace(waker); + drop(state); + drop(old); + Poll::Pending + } +} + +// No operation relies on a pinned location for the receiver batch or its values. +impl Unpin for UnboundedReceiver {} diff --git a/asyncband/src/mpsc/unbounded/sender.rs b/asyncband/src/mpsc/unbounded/sender.rs new file mode 100644 index 0000000..a90589c --- /dev/null +++ b/asyncband/src/mpsc/unbounded/sender.rs @@ -0,0 +1,88 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::fmt; +use std::sync::Arc; + +use super::State; +use crate::internal::mutex::Mutex; +use crate::mpsc::SendError; + +/// The sending endpoint of an unbounded mpsc channel. +/// +/// Instances are created by the [`unbounded`](crate::mpsc::unbounded) function. +pub struct UnboundedSender { + shared: Arc>>, +} + +impl Clone for UnboundedSender { + fn clone(&self) -> Self { + self.shared.lock().senders += 1; + Self { + shared: self.shared.clone(), + } + } +} + +impl fmt::Debug for UnboundedSender { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UnboundedSender").finish_non_exhaustive() + } +} + +impl Drop for UnboundedSender { + fn drop(&mut self) { + let wake = { + let mut state = self.shared.lock(); + state.senders -= 1; + if state.senders == 0 { + state.recv_waker.take() + } else { + None + } + }; + if let Some(waker) = wake { + waker.wake(); + } + } +} + +impl UnboundedSender { + pub(super) fn new(shared: Arc>>) -> Self { + Self { shared } + } + + /// Enqueues a message without waiting for capacity. + /// + /// This operation is synchronous because the channel has no capacity limit. If the receiver has + /// been dropped, the returned error contains `value`. Success means the message was queued; + /// it does not guarantee that the receiver will consume it before being dropped. + pub fn send(&self, value: T) -> Result<(), SendError> { + let waker = { + let mut state = self.shared.lock(); + if !state.receiver { + return Err(SendError::new(value)); + } + state.buffer.push(value); + state.recv_waker.take() + }; + if let Some(waker) = waker { + waker.wake(); + } + Ok(()) + } +} diff --git a/tests-integration/tests/mpsc_test/main.rs b/tests-integration/tests/mpsc_test/main.rs index f2ab74f..a8c1378 100644 --- a/tests-integration/tests/mpsc_test/main.rs +++ b/tests-integration/tests/mpsc_test/main.rs @@ -42,7 +42,9 @@ fn unbounded_try_recv_preserves_order_and_reports_state() { tx.send(i).unwrap(); } - for i in 0..4 { + assert_eq!(rx.try_recv(), Ok(0)); + tx.send(4).unwrap(); + for i in 1..5 { assert_eq!(rx.try_recv(), Ok(i)); } assert_eq!(rx.try_recv(), Err(TryRecvError::Empty)); From 960d382eb57c79fc8d345dbf7edece76896ad07f Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 18:58:19 +0800 Subject: [PATCH 02/26] ci: compile benchmarks without running them Signed-off-by: tison --- .github/workflows/ci.yml | 4 ++-- xtask/src/main.rs | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 725aa68..08b0075 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,7 @@ jobs: run: cargo x miri benchmark: - name: Run benchmarks + name: Compile benchmarks runs-on: ubuntu-24.04 timeout-minutes: 30 env: @@ -126,7 +126,7 @@ jobs: --profile minimal --no-self-update - uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - - run: cargo x bench + - run: cargo x bench --no-run required: name: Required diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 6a16125..9074ed0 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -68,11 +68,14 @@ enum SubCommand { } #[derive(Parser)] -struct CommandBench; +struct CommandBench { + #[arg(long, help = "Compile benchmarks without running them.")] + no_run: bool, +} impl CommandBench { fn run(self) { - run_command(make_bench_cmd()); + run_command(make_bench_cmd(self.no_run)); } } @@ -356,9 +359,12 @@ fn classify_release_type(baseline: &Version, release: &Version) -> SemverRelease } } -fn make_bench_cmd() -> StdCommand { +fn make_bench_cmd(no_run: bool) -> StdCommand { let mut cmd = find_command("cargo"); cmd.args(["bench", "--workspace", "--all-features", "--bench", "*"]); + if no_run { + cmd.arg("--no-run"); + } cmd } From 8733748a0331f9ab58daa439d945b82ce7c5c1e1 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 19:30:24 +0800 Subject: [PATCH 03/26] ci: compile benchmarks in the check job Signed-off-by: tison --- .github/workflows/ci.yml | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08b0075..32a27aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,8 @@ jobs: - run: cargo x lint - name: Check feature matrix run: cargo x check + - name: Compile benchmarks + run: cargo x bench --no-run test: name: Run tests @@ -112,28 +114,11 @@ jobs: - name: Run Miri tests run: cargo x miri - benchmark: - name: Compile benchmarks - runs-on: ubuntu-24.04 - timeout-minutes: 30 - env: - RUSTUP_TOOLCHAIN: stable - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Install stable toolchain - run: >- - rustup toolchain install stable - --profile minimal - --no-self-update - - uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - - run: cargo x bench --no-run - required: name: Required runs-on: ubuntu-24.04 if: ${{ always() }} needs: - - benchmark - check - miri - test @@ -141,8 +126,7 @@ jobs: - name: Guardian run: | if [[ ! ( \ - "${{ needs.benchmark.result }}" == "success" \ - && "${{ needs.check.result }}" == "success" \ + "${{ needs.check.result }}" == "success" \ && "${{ needs.miri.result }}" == "success" \ && "${{ needs.test.result }}" == "success" \ ) ]]; then From 09f8ecba207f6f9d1db7a896850021dd5755b59c Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 19:30:27 +0800 Subject: [PATCH 04/26] test(mpsc): keep buffer reclamation checks inline Signed-off-by: tison --- asyncband/src/mpsc/unbounded/buffer.rs | 54 ++++++++++++- asyncband/src/mpsc/unbounded/buffer_tests.rs | 85 -------------------- 2 files changed, 52 insertions(+), 87 deletions(-) delete mode 100644 asyncband/src/mpsc/unbounded/buffer_tests.rs diff --git a/asyncband/src/mpsc/unbounded/buffer.rs b/asyncband/src/mpsc/unbounded/buffer.rs index 29fbd54..ee195a9 100644 --- a/asyncband/src/mpsc/unbounded/buffer.rs +++ b/asyncband/src/mpsc/unbounded/buffer.rs @@ -78,5 +78,55 @@ pub fn pop_batch(batch: &mut VecDeque) -> T { } #[cfg(test)] -#[path = "buffer_tests.rs"] -mod tests; +mod tests { + use std::collections::VecDeque; + + use super::Buffer; + use super::SEGMENT_BYTES; + use super::pop_batch; + + fn allocated_bytes(buffer: &Buffer, batch: &VecDeque) -> usize { + let slots = batch.capacity() + + buffer.writable.capacity() + + buffer.sealed.iter().map(VecDeque::capacity).sum::(); + slots * size_of::() + } + + fn receive(buffer: &mut Buffer, batch: &mut VecDeque) -> T { + if batch.is_empty() { + buffer.refill(batch); + } + pop_batch(batch) + } + + #[test] + fn a_partial_drain_reclaims_segments_and_preserves_new_sends() { + let mut buffer = Buffer::new(); + let mut batch = VecDeque::new(); + for value in 0..1024usize { + buffer.push([value; 128]); + } + let peak = allocated_bytes(&buffer, &batch); + for value in 0..512 { + assert_eq!(receive(&mut buffer, &mut batch), [value; 128]); + } + assert!(allocated_bytes(&buffer, &batch) <= peak * 3 / 4); + // This value must stay behind both the current batch and the sealed segments. + buffer.push([1024; 128]); + for value in 512..=1024 { + assert_eq!(receive(&mut buffer, &mut batch), [value; 128]); + } + assert!(allocated_bytes(&buffer, &batch) <= 2 * SEGMENT_BYTES); + buffer.refill(&mut batch); + assert!(batch.is_empty()); + } + + #[test] + fn oversized_inline_values_release_the_allocation_on_the_last_receive() { + let mut buffer = Buffer::new(); + let mut batch = VecDeque::new(); + buffer.push([7u8; SEGMENT_BYTES + 1]); + assert_eq!(receive(&mut buffer, &mut batch), [7u8; SEGMENT_BYTES + 1]); + assert_eq!(allocated_bytes(&buffer, &batch), 0); + } +} diff --git a/asyncband/src/mpsc/unbounded/buffer_tests.rs b/asyncband/src/mpsc/unbounded/buffer_tests.rs deleted file mode 100644 index 8be63c3..0000000 --- a/asyncband/src/mpsc/unbounded/buffer_tests.rs +++ /dev/null @@ -1,85 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::collections::VecDeque; - -use super::Buffer; -use super::SEGMENT_BYTES; -use super::pop_batch; - -fn allocated_bytes(buffer: &Buffer, batch: &VecDeque) -> usize { - let slots = batch.capacity() - + buffer.writable.capacity() - + buffer.sealed.iter().map(VecDeque::capacity).sum::(); - slots * size_of::() -} - -fn receive(buffer: &mut Buffer, batch: &mut VecDeque) -> T { - if batch.is_empty() { - buffer.refill(batch); - } - pop_batch(batch) -} - -#[test] -fn a_partial_drain_reclaims_segments_and_preserves_new_sends() { - let mut buffer = Buffer::new(); - let mut batch = VecDeque::new(); - for value in 0..1024usize { - buffer.push([value; 128]); - } - let peak = allocated_bytes(&buffer, &batch); - for value in 0..512 { - assert_eq!(receive(&mut buffer, &mut batch), [value; 128]); - } - assert!(allocated_bytes(&buffer, &batch) <= peak * 3 / 4); - // This value must stay behind both the current batch and the sealed segments. - buffer.push([1024; 128]); - for value in 512..=1024 { - assert_eq!(receive(&mut buffer, &mut batch), [value; 128]); - } - assert!(allocated_bytes(&buffer, &batch) <= 2 * SEGMENT_BYTES); - buffer.refill(&mut batch); - assert!(batch.is_empty()); -} - -#[test] -fn small_batches_are_reused_on_refill() { - let mut buffer = Buffer::new(); - let mut batch = VecDeque::new(); - for value in 0..32usize { - buffer.push(value); - } - assert_eq!(receive(&mut buffer, &mut batch), 0); - let capacity = batch.capacity(); - for value in 1..32 { - assert_eq!(receive(&mut buffer, &mut batch), value); - } - assert_eq!(batch.capacity(), capacity); - buffer.push(32); - assert_eq!(receive(&mut buffer, &mut batch), 32); - assert_eq!(buffer.writable.capacity(), capacity); -} - -#[test] -fn oversized_inline_values_release_the_allocation_on_the_last_receive() { - let mut buffer = Buffer::new(); - let mut batch = VecDeque::new(); - buffer.push([7u8; SEGMENT_BYTES + 1]); - assert_eq!(receive(&mut buffer, &mut batch), [7u8; SEGMENT_BYTES + 1]); - assert_eq!(allocated_bytes(&buffer, &batch), 0); -} From 9a7d91ac5baca479398200f2e0caa1018826ade7 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:36:18 +0800 Subject: [PATCH 05/26] bench(watch): remove future erasure from adapters Signed-off-by: tison --- benchmarks/ecosystem/watch/adapters.rs | 25 ++++++++++----------- benchmarks/ecosystem/watch/paths.rs | 30 +++++++------------------- 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/benchmarks/ecosystem/watch/adapters.rs b/benchmarks/ecosystem/watch/adapters.rs index 1becc1a..f5a9b6e 100644 --- a/benchmarks/ecosystem/watch/adapters.rs +++ b/benchmarks/ecosystem/watch/adapters.rs @@ -16,7 +16,6 @@ // under the License. use std::future::Future; -use std::pin::Pin; pub struct Asyncband; pub struct Tokio; @@ -28,8 +27,8 @@ pub trait Watch: Send + Sync + 'static { fn channel(receiver_count: usize) -> (Self::Sender, Vec); fn send(sender: &Self::Sender, value: usize); fn get(receiver: &Self::Receiver) -> usize; - fn recv(receiver: &mut Self::Receiver) -> Pin + '_>>; - fn changed(receiver: &mut Self::Receiver) -> Pin + '_>>; + fn recv(receiver: &mut Self::Receiver) -> impl Future; + fn changed(receiver: &mut Self::Receiver) -> impl Future; } impl Watch for Asyncband { @@ -52,12 +51,12 @@ impl Watch for Asyncband { receiver.get() } - fn recv(receiver: &mut Self::Receiver) -> Pin + '_>> { - Box::pin(async move { receiver.recv().await.unwrap() }) + async fn recv(receiver: &mut Self::Receiver) -> usize { + receiver.recv().await.unwrap() } - fn changed(receiver: &mut Self::Receiver) -> Pin + '_>> { - Box::pin(async move { receiver.changed().await.unwrap() }) + async fn changed(receiver: &mut Self::Receiver) { + receiver.changed().await.unwrap(); } } @@ -81,14 +80,12 @@ impl Watch for Tokio { *receiver.borrow() } - fn recv(receiver: &mut Self::Receiver) -> Pin + '_>> { - Box::pin(async move { - receiver.changed().await.unwrap(); - *receiver.borrow_and_update() - }) + async fn recv(receiver: &mut Self::Receiver) -> usize { + receiver.changed().await.unwrap(); + *receiver.borrow_and_update() } - fn changed(receiver: &mut Self::Receiver) -> Pin + '_>> { - Box::pin(async move { receiver.changed().await.unwrap() }) + async fn changed(receiver: &mut Self::Receiver) { + receiver.changed().await.unwrap(); } } diff --git a/benchmarks/ecosystem/watch/paths.rs b/benchmarks/ecosystem/watch/paths.rs index 9ea3e02..f20d253 100644 --- a/benchmarks/ecosystem/watch/paths.rs +++ b/benchmarks/ecosystem/watch/paths.rs @@ -20,11 +20,6 @@ // continuing. The recv adapter combines Tokio's changed and borrow_and_update operations to match // Asyncband's owned receive contract. -use std::future::Future; -use std::pin::Pin; -use std::task::Context; -use std::task::Poll; - use divan::Bencher; use divan::black_box; @@ -32,24 +27,12 @@ use super::adapters::Asyncband; use super::adapters::Tokio; use super::adapters::Watch; use crate::support::bench_context; +use crate::support::poll_pending; +use crate::support::poll_pinned_ready; use crate::support::poll_ready; const RECEIVER_COUNTS: &[usize] = &[1, 2, 4, 8, 32]; -fn poll_erased_pending(mut future: Pin<&mut dyn Future>, context: &mut Context<'_>) { - assert!(future.as_mut().poll(context).is_pending()); -} - -fn poll_erased_ready( - mut future: Pin<&mut dyn Future>, - context: &mut Context<'_>, -) -> T { - match future.as_mut().poll(context) { - Poll::Ready(output) => output, - Poll::Pending => panic!("benchmark future should be ready"), - } -} - #[divan::bench(types = [Asyncband, Tokio])] fn get_current(bencher: Bencher) { let (sender, mut receivers) = C::channel(1); @@ -96,14 +79,17 @@ fn notify_pending_fanout(bencher: Bencher, receiver_count: usize) { let (sender, mut receivers) = C::channel(receiver_count); bencher.bench_local(|| { - let mut changed = receivers.iter_mut().map(C::changed).collect::>(); + let mut changed = receivers + .iter_mut() + .map(|receiver| Box::pin(C::changed(receiver))) + .collect::>(); for future in &mut changed { - poll_erased_pending(future.as_mut(), &mut context); + poll_pending(future.as_mut(), &mut context); } C::send(&sender, black_box(1)); for mut future in changed { - poll_erased_ready(future.as_mut(), &mut context); + poll_pinned_ready(future.as_mut(), &mut context); } }); } From ef6646cbc345885a0f90302a0ca0e140a1b06cd6 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:36:23 +0800 Subject: [PATCH 06/26] bench(waitgroup): remove incomplete worker round trips Signed-off-by: tison --- benchmarks/asyncband/waitgroup/wait.rs | 16 ---------------- benchmarks/ecosystem/waitgroup.rs | 15 --------------- 2 files changed, 31 deletions(-) diff --git a/benchmarks/asyncband/waitgroup/wait.rs b/benchmarks/asyncband/waitgroup/wait.rs index c401aee..8affbeb 100644 --- a/benchmarks/asyncband/waitgroup/wait.rs +++ b/benchmarks/asyncband/waitgroup/wait.rs @@ -27,8 +27,6 @@ use crate::support::poll_pending; use crate::support::poll_pinned_ready; const WORKER_COUNTS: &[usize] = &[1, 8, 32]; -const THREAD_COUNTS: &[usize] = &[1, 2, 8, 32]; -const CONTENDED_SAMPLE_SIZE: u32 = 256; #[divan::bench] fn ready_empty(bencher: Bencher) { @@ -41,20 +39,6 @@ fn ready_empty(bencher: Bencher) { }); } -#[divan::bench] -fn worker_round_trip(bencher: Bencher) { - let root = WaitGroup::new(); - - bencher.bench_local(|| black_box(root.clone())); -} - -#[divan::bench(threads = THREAD_COUNTS, sample_size = CONTENDED_SAMPLE_SIZE)] -fn worker_round_trip_contended(bencher: Bencher) { - let root = WaitGroup::new(); - - bencher.bench(|| black_box(root.clone())); -} - #[divan::bench] fn cancel_pending(bencher: Bencher) { let mut context = bench_context(); diff --git a/benchmarks/ecosystem/waitgroup.rs b/benchmarks/ecosystem/waitgroup.rs index 5b812bc..445667f 100644 --- a/benchmarks/ecosystem/waitgroup.rs +++ b/benchmarks/ecosystem/waitgroup.rs @@ -117,21 +117,6 @@ fn cancel_pending(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, WaitgroupRs])] -fn worker_round_trip(bencher: Bencher) { - let group = C::new(); - - bencher.bench_local(|| black_box(C::worker(&group))); -} - -#[divan::bench(types = [Asyncband, WaitgroupRs])] -fn nested_worker_round_trip(bencher: Bencher) { - let group = C::new(); - let worker = C::worker(&group); - - bencher.bench_local(|| black_box(worker.clone())); -} - #[divan::bench(types = [Asyncband, WaitgroupRs], args = WORKER_COUNTS)] fn worker_batch(bencher: Bencher, worker_count: usize) { bencher.bench_local(|| { From e64eca776f4b88a85a636872abc1ef7ee66ebe0b Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:36:26 +0800 Subject: [PATCH 07/26] bench(oneshot): use the shared task waker Signed-off-by: tison --- benchmarks/asyncband/oneshot/send.rs | 30 +++++++++++----------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/benchmarks/asyncband/oneshot/send.rs b/benchmarks/asyncband/oneshot/send.rs index 9b74b6b..676b7ce 100644 --- a/benchmarks/asyncband/oneshot/send.rs +++ b/benchmarks/asyncband/oneshot/send.rs @@ -15,48 +15,42 @@ // specific language governing permissions and limitations // under the License. -use std::future::Future; use std::future::IntoFuture; -use std::pin::Pin; -use std::task::Context; -use std::task::Poll; -use std::task::Waker; +use std::pin::pin; use asyncband::oneshot::channel; use divan::Bencher; use divan::black_box; +use crate::support::bench_context; +use crate::support::poll_pending; +use crate::support::poll_pinned_ready; + #[divan::bench] fn send_before_poll(bencher: Bencher) { - let mut context = Context::from_waker(Waker::noop()); + let mut context = bench_context(); bencher.bench_local(|| { let (sender, receiver) = black_box(channel()); - let mut receiver = receiver.into_future(); + let mut receiver = pin!(receiver.into_future()); sender.send(black_box(1usize)).unwrap(); - match Pin::new(&mut receiver).poll(&mut context) { - Poll::Ready(Ok(value)) => black_box(value), - result => panic!("unexpected receive result: {result:?}"), - } + black_box(poll_pinned_ready(receiver.as_mut(), &mut context).unwrap()) }); } #[divan::bench] fn poll_before_send(bencher: Bencher) { - let mut context = Context::from_waker(Waker::noop()); + let mut context = bench_context(); bencher.bench_local(|| { let (sender, receiver) = black_box(channel()); - let mut receiver = receiver.into_future(); + let mut receiver = pin!(receiver.into_future()); - assert_eq!(Pin::new(&mut receiver).poll(&mut context), Poll::Pending); + poll_pending(receiver.as_mut(), &mut context); sender.send(black_box(1usize)).unwrap(); - match Pin::new(&mut receiver).poll(&mut context) { - Poll::Ready(Ok(value)) => black_box(value), - result => panic!("unexpected receive result: {result:?}"), - } + black_box(poll_pinned_ready(receiver.as_mut(), &mut context).unwrap()) }); } From 82b230e2f1acf7b890ef6700c62e942a558aa4f1 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:36:28 +0800 Subject: [PATCH 08/26] bench(singleflight): focus on complete caller lifecycles Signed-off-by: tison --- benchmarks/asyncband/singleflight/hot.rs | 33 ------------------------ 1 file changed, 33 deletions(-) diff --git a/benchmarks/asyncband/singleflight/hot.rs b/benchmarks/asyncband/singleflight/hot.rs index 2affbbc..2692e74 100644 --- a/benchmarks/asyncband/singleflight/hot.rs +++ b/benchmarks/asyncband/singleflight/hot.rs @@ -29,39 +29,6 @@ use crate::support::poll_pending; use crate::support::poll_pinned_ready; use crate::support::wait_until_open; -// Isolates duplicate admission: the leader is already pending and future construction happens -// outside the timed section. A whole batch is needed because one admission is below the timer's -// useful resolution; sample_size=1 lets Divan drop the batch before the next sample instead of -// growing a synthetic waiter backlog across samples. -#[divan::bench(args = BATCH_SIZES, sample_count = 100, sample_size = 1)] -fn join_in_flight(bencher: Bencher, duplicate_count: usize) { - let group = BenchGroup::default(); - let gate = Cell::new(false); - let mut context = bench_context(); - let mut leader = Box::pin(group.work(0, || async { - wait_until_open(&gate).await; - black_box(1usize) - })); - poll_pending(leader.as_mut(), &mut context); - - bencher - .with_inputs(|| { - (0..duplicate_count) - .map(|_| Box::pin(group.work(0, || async { unreachable!() }))) - .collect::>() - }) - .counter(ItemsCount::new(duplicate_count)) - .bench_local_values(|mut duplicates| { - for duplicate in &mut duplicates { - poll_pending(duplicate.as_mut(), &mut context); - } - duplicates - }); - - gate.set(true); - black_box(poll_pinned_ready(leader.as_mut(), &mut context)); -} - // Measures the complete successful fan-in: one leader is suspended, every duplicate joins it, and // all callers receive the cloned result after the leader completes. #[divan::bench(args = BATCH_SIZES, sample_size = BATCH_SAMPLE_SIZE)] From f1e13942cdbb659ecbc4875b375760b146b9d0ec Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:36:32 +0800 Subject: [PATCH 09/26] bench(once_map): poll cached computations once Signed-off-by: tison --- benchmarks/asyncband/once_map/hot.rs | 6 +++--- benchmarks/asyncband/support.rs | 12 ------------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/benchmarks/asyncband/once_map/hot.rs b/benchmarks/asyncband/once_map/hot.rs index f607f85..6d85bd3 100644 --- a/benchmarks/asyncband/once_map/hot.rs +++ b/benchmarks/asyncband/once_map/hot.rs @@ -26,7 +26,7 @@ use super::support::distributed_absent_key; use super::support::distributed_ready_key; use super::support::ready_map; use crate::support::bench_context; -use crate::support::spin_poll_ready; +use crate::support::poll_ready; #[divan::bench(threads = THREAD_COUNTS, sample_size = CONTENDED_SAMPLE_SIZE)] fn get_hit_same_key(bencher: Bencher) { @@ -65,7 +65,7 @@ fn compute_hit_same_key(bencher: Bencher) { let map = ready_map(1); bencher.bench(|| { let mut context = bench_context(); - black_box(spin_poll_ready( + black_box(poll_ready( map.compute(black_box(0), || async { unreachable!() }), &mut context, )) @@ -83,7 +83,7 @@ fn compute_hit_distributed(bencher: Bencher, ready_entries: usize) { .with_inputs(|| distributed_ready_key(ready_entries)) .bench_values(|key| { let mut context = bench_context(); - black_box(spin_poll_ready( + black_box(poll_ready( map.compute(black_box(key), || async { unreachable!() }), &mut context, )) diff --git a/benchmarks/asyncband/support.rs b/benchmarks/asyncband/support.rs index cba7cbd..60382cd 100644 --- a/benchmarks/asyncband/support.rs +++ b/benchmarks/asyncband/support.rs @@ -63,18 +63,6 @@ pub(super) fn poll_pending(mut future: Pin<&mut F>, context: &mut Con assert!(future.as_mut().poll(context).is_pending()); } -// Polls the future to completion, yielding between polls so a leader running on another thread can -// make progress. The bench waker never wakes, so pending futures must be re-polled unconditionally. -pub(super) fn spin_poll_ready(future: F, context: &mut Context<'_>) -> F::Output { - let mut future = pin!(future); - loop { - match future.as_mut().poll(context) { - Poll::Ready(output) => return output, - Poll::Pending => std::thread::yield_now(), - } - } -} - static NEXT_THREAD_SLOT: AtomicUsize = AtomicUsize::new(0); thread_local! { From 14accd52f6a84b71c238a189fa43e147cee47bf6 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:36:35 +0800 Subject: [PATCH 10/26] bench(once_map): consolidate entry removal measurements Signed-off-by: tison --- benchmarks/asyncband/once_map/maintenance.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/benchmarks/asyncband/once_map/maintenance.rs b/benchmarks/asyncband/once_map/maintenance.rs index dad6590..9564293 100644 --- a/benchmarks/asyncband/once_map/maintenance.rs +++ b/benchmarks/asyncband/once_map/maintenance.rs @@ -25,17 +25,6 @@ use super::support::READY_ENTRY_COUNTS; use super::support::ready_map; use crate::support::defer_input_drop; -#[divan::bench(args = NONEMPTY_ENTRY_COUNTS, sample_size = BATCH_SAMPLE_SIZE)] -fn discard_hit(bencher: Bencher, ready_entries: usize) { - let key = ready_entries / 2; - bencher - .with_inputs(|| ready_map(ready_entries)) - .bench_local_values(|map| { - map.discard(black_box(&key)); - defer_input_drop(map, ()) - }); -} - #[divan::bench(args = NONEMPTY_ENTRY_COUNTS, sample_size = BATCH_SAMPLE_SIZE)] fn remove_hit(bencher: Bencher, ready_entries: usize) { let key = ready_entries / 2; @@ -47,12 +36,6 @@ fn remove_hit(bencher: Bencher, ready_entries: usize) { }); } -#[divan::bench(args = READY_ENTRY_COUNTS, sample_size = FAST_SAMPLE_SIZE)] -fn discard_miss(bencher: Bencher, ready_entries: usize) { - let map = ready_map(ready_entries); - bencher.bench_local(|| map.discard(black_box(&usize::MAX))); -} - #[divan::bench(args = READY_ENTRY_COUNTS, sample_size = FAST_SAMPLE_SIZE)] fn remove_miss(bencher: Bencher, ready_entries: usize) { let map = ready_map(ready_entries); From 26ba4077ef8a6dca5c62306bd8862e897fb7bfae Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:36:38 +0800 Subject: [PATCH 11/26] bench(pool): use one lazy construction configuration Signed-off-by: tison --- benchmarks/asyncband/pool/bounded.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/benchmarks/asyncband/pool/bounded.rs b/benchmarks/asyncband/pool/bounded.rs index 65f4eea..7441c26 100644 --- a/benchmarks/asyncband/pool/bounded.rs +++ b/benchmarks/asyncband/pool/bounded.rs @@ -29,8 +29,6 @@ use crate::support::poll_pending; use crate::support::poll_pinned_ready; use crate::support::poll_ready; -const CAPACITIES: &[usize] = &[1, 32, 1024, usize::MAX]; - struct Manager; impl ManageObject for Manager { @@ -50,11 +48,11 @@ impl ManageObject for Manager { } } -#[divan::bench(args = CAPACITIES)] -fn construct_bounded(bencher: Bencher, capacity: usize) { +#[divan::bench] +fn construct_bounded(bencher: Bencher) { bencher.bench_local(|| { black_box(bounded::Pool::new( - bounded::PoolConfig::new(black_box(capacity)), + bounded::PoolConfig::new(black_box(32)), Manager, )) }); From 6019df6ba1a5f82594f4f1a3b8c592542a3011a0 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:40:21 +0800 Subject: [PATCH 12/26] test: share waker and deadlock support across integration suites Signed-off-by: tison --- tests-integration/src/lib.rs | 106 ++++++++ .../tests/broadcast_mpmc_unbounded_test.rs | 47 ++-- tests-integration/tests/completion_test.rs | 228 ++++++------------ tests-integration/tests/condvar_test.rs | 29 +-- tests-integration/tests/event_test.rs | 70 ++---- tests-integration/tests/latch_test.rs | 27 +-- .../tests/mpsc_test/backpressure.rs | 7 +- .../tests/mpsc_test/callbacks.rs | 27 +-- .../tests/mpsc_test/concurrency.rs | 5 +- tests-integration/tests/mpsc_test/main.rs | 7 +- .../tests/mpsc_test/reservation.rs | 17 +- tests-integration/tests/mpsc_test/support.rs | 77 ------ .../tests/pool_acquire_cancelled_test.rs | 30 +-- tests-integration/tests/semaphore_test.rs | 27 +-- tests-integration/tests/unsafe_paths_test.rs | 6 +- tests-integration/tests/waitgroup_test.rs | 22 +- tests-integration/tests/watch_test.rs | 152 +++--------- 17 files changed, 311 insertions(+), 573 deletions(-) diff --git a/tests-integration/src/lib.rs b/tests-integration/src/lib.rs index 95f6cdb..00f2045 100644 --- a/tests-integration/src/lib.rs +++ b/tests-integration/src/lib.rs @@ -17,9 +17,14 @@ use std::future::Future; use std::pin::Pin; +use std::sync::Arc; +use std::sync::Mutex; use std::sync::OnceLock; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; use std::task::Context; use std::task::Poll; +use std::task::Wake; use std::task::Waker; use tokio::runtime::Runtime; @@ -34,3 +39,104 @@ pub fn test_runtime() -> &'static Runtime { static RUNTIME: OnceLock = OnceLock::new(); RUNTIME.get_or_init(|| Runtime::new().unwrap()) } + +pub fn expect_ready(poll: Poll) -> T { + match poll { + Poll::Ready(value) => value, + Poll::Pending => panic!("future should be ready"), + } +} + +pub fn poll_with(future: Pin<&mut F>, waker: &Waker) -> Poll { + future.poll(&mut Context::from_waker(waker)) +} + +#[derive(Default)] +pub struct WakeCounter(AtomicUsize); + +impl WakeCounter { + pub fn new() -> (Waker, Arc) { + let counter = Arc::new(Self::default()); + (Waker::from(counter.clone()), counter) + } + + pub fn count(&self) -> usize { + self.0.load(Ordering::Relaxed) + } + + pub fn take(&self) -> usize { + self.0.swap(0, Ordering::Relaxed) + } +} + +impl Wake for WakeCounter { + fn wake(self: Arc) { + self.wake_by_ref(); + } + + fn wake_by_ref(self: &Arc) { + self.0.fetch_add(1, Ordering::Relaxed); + } +} + +pub struct PanicWake; + +impl Wake for PanicWake { + fn wake(self: Arc) { + panic!("wake failed"); + } +} + +pub fn waker_on_wake(callback: impl FnOnce() + Send + 'static) -> Waker { + struct OnWake(Mutex>>); + + impl Wake for OnWake { + fn wake(self: Arc) { + let callback = self.0.lock().unwrap().take(); + if let Some(callback) = callback { + callback(); + } + } + } + + Waker::from(Arc::new(OnWake(Mutex::new(Some(Box::new(callback)))))) +} + +pub fn waker_on_drop(callback: impl FnOnce() + Send + 'static) -> Waker { + struct OnDrop(Mutex>>); + + // Only destruction runs the callback; waking consumes the reference as usual. + #[allow(clippy::manual_noop_waker)] + impl Wake for OnDrop { + fn wake(self: Arc) {} + } + + impl Drop for OnDrop { + fn drop(&mut self) { + if let Some(callback) = self.0.get_mut().unwrap().take() { + callback(); + } + } + } + + Waker::from(Arc::new(OnDrop(Mutex::new(Some(Box::new(callback)))))) +} + +pub fn assert_completes_without_deadlock(test: impl FnOnce() + Send + 'static) { + let (finished_tx, finished_rx) = std::sync::mpsc::channel(); + let worker = std::thread::spawn(move || { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(test)); + finished_tx.send(result).unwrap(); + }); + #[cfg(not(miri))] + let result = finished_rx + .recv_timeout(std::time::Duration::from_secs(10)) + .expect("test did not finish"); + // Miri detects deadlock itself; its interpretation time must not determine test success. + #[cfg(miri)] + let result = finished_rx.recv().expect("test did not finish"); + worker.join().unwrap(); + if let Err(payload) = result { + std::panic::resume_unwind(payload); + } +} diff --git a/tests-integration/tests/broadcast_mpmc_unbounded_test.rs b/tests-integration/tests/broadcast_mpmc_unbounded_test.rs index f104e8b..9012051 100644 --- a/tests-integration/tests/broadcast_mpmc_unbounded_test.rs +++ b/tests-integration/tests/broadcast_mpmc_unbounded_test.rs @@ -20,20 +20,12 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use std::task::Context; use std::task::Poll; -use std::task::Wake; use std::task::Waker; use std::thread; -use std::time::Duration; use asyncband::broadcast::mpmc::*; - -struct TrackWake(AtomicUsize); - -impl Wake for TrackWake { - fn wake(self: Arc) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} +use tests_integration::WakeCounter; +use tests_integration::assert_completes_without_deadlock; /// A payload whose destructor re-enters the channel it was sent through. struct Reentrant { @@ -329,9 +321,7 @@ fn panicking_clone_leaves_the_channel_consistent() { #[test] fn message_destructors_run_outside_the_channel_lock() { - let (finished_tx, finished_rx) = std::sync::mpsc::channel(); - - let worker = thread::spawn(move || { + assert_completes_without_deadlock(|| { let (tx, mut rx1) = unbounded(); let rx2 = tx.subscribe(); @@ -354,20 +344,13 @@ fn message_destructors_run_outside_the_channel_lock() { channel: Some(tx.clone()), }); drop(tx); - - finished_tx.send(()).unwrap(); }); - - finished_rx - .recv_timeout(Duration::from_secs(10)) - .expect("a message destructor deadlocked against the channel lock"); - worker.join().unwrap(); } #[test] fn send_wakes_a_parked_receiver_exactly_once() { let (tx, mut rx) = unbounded(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let mut context = Context::from_waker(&waker); let mut recv = Box::pin(rx.recv()); @@ -376,14 +359,14 @@ fn send_wakes_a_parked_receiver_exactly_once() { tx.send(42); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); assert_eq!(recv.as_mut().poll(&mut context), Poll::Ready(Ok(42))); } #[test] fn cancelled_recv_releases_its_waker() { let (tx, mut rx) = unbounded::<()>(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let baseline = Arc::strong_count(&tracker); let mut context = Context::from_waker(&waker); @@ -396,7 +379,7 @@ fn cancelled_recv_releases_its_waker() { assert_eq!(Arc::strong_count(&tracker), baseline); tx.send(()); - assert_eq!(tracker.0.load(Ordering::Relaxed), 0); + assert_eq!(tracker.count(), 0); assert_eq!(rx.try_recv(), Ok(())); } @@ -404,7 +387,7 @@ fn cancelled_recv_releases_its_waker() { fn dropping_a_woken_recv_keeps_another_receivers_waiter() { let (tx, mut rx1) = unbounded::(); let mut rx2 = tx.subscribe(); - let first = Arc::new(TrackWake(AtomicUsize::new(0))); + let first = Arc::new(WakeCounter::default()); let waker = Waker::from(first.clone()); let mut context = Context::from_waker(&waker); let mut recv1 = Box::pin(rx1.recv()); @@ -412,10 +395,10 @@ fn dropping_a_woken_recv_keeps_another_receivers_waiter() { assert!(recv1.as_mut().poll(&mut context).is_pending()); tx.send(1); - assert_eq!(first.0.load(Ordering::Relaxed), 1); + assert_eq!(first.count(), 1); assert_eq!(rx2.try_recv(), Ok(1)); - let second = Arc::new(TrackWake(AtomicUsize::new(0))); + let second = Arc::new(WakeCounter::default()); let waker = Waker::from(second.clone()); let mut context = Context::from_waker(&waker); let mut recv2 = Box::pin(rx2.recv()); @@ -425,14 +408,14 @@ fn dropping_a_woken_recv_keeps_another_receivers_waiter() { drop(recv1); tx.send(2); - assert_eq!(second.0.load(Ordering::Relaxed), 1); + assert_eq!(second.count(), 1); } #[test] fn parked_recv_wakes_when_the_last_sender_drops() { let (tx, mut rx) = unbounded::<()>(); let extra = tx.clone(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let mut context = Context::from_waker(&waker); let mut recv = Box::pin(rx.recv()); @@ -440,10 +423,10 @@ fn parked_recv_wakes_when_the_last_sender_drops() { assert!(recv.as_mut().poll(&mut context).is_pending()); drop(tx); - assert_eq!(tracker.0.load(Ordering::Relaxed), 0); + assert_eq!(tracker.count(), 0); drop(extra); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); drop(recv); assert_eq!(rx.try_recv(), Err(TryRecvError::Disconnected)); @@ -452,7 +435,7 @@ fn parked_recv_wakes_when_the_last_sender_drops() { #[test] fn parked_recv_prefers_buffered_messages_over_disconnection() { let (tx, mut rx) = unbounded(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker); let mut context = Context::from_waker(&waker); let mut recv = Box::pin(rx.recv()); diff --git a/tests-integration/tests/completion_test.rs b/tests-integration/tests/completion_test.rs index 0bfd5b0..c4d3d9a 100644 --- a/tests-integration/tests/completion_test.rs +++ b/tests-integration/tests/completion_test.rs @@ -16,87 +16,22 @@ // under the License. use std::cell::Cell; -use std::future::Future; -use std::pin::Pin; use std::sync::Arc; use std::sync::Barrier; -use std::sync::Mutex; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; -use std::task::Context; use std::task::Poll; -use std::task::Wake; use std::task::Waker; use std::thread; -use std::time::Duration; use asyncband::completion; +use tests_integration::PanicWake; +use tests_integration::WakeCounter; +use tests_integration::assert_completes_without_deadlock; +use tests_integration::poll_with; +use tests_integration::waker_on_drop; +use tests_integration::waker_on_wake; struct NotClone(String); -struct TrackWake(AtomicUsize); - -impl Wake for TrackWake { - fn wake(self: Arc) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} - -struct PanicWake; - -impl Wake for PanicWake { - fn wake(self: Arc) { - panic!("wake failed"); - } -} - -struct WakeCallback(Mutex>>); - -impl Wake for WakeCallback { - fn wake(self: Arc) { - let callback = self.0.lock().unwrap().take(); - if let Some(callback) = callback { - callback(); - } - } -} - -struct DropCallbackWake(Mutex>>); - -// This test needs a custom waker whose final `Arc` drop is observable. -#[allow(clippy::manual_noop_waker)] -impl Wake for DropCallbackWake { - fn wake(self: Arc) {} -} - -impl Drop for DropCallbackWake { - fn drop(&mut self) { - if let Some(callback) = self.0.get_mut().unwrap().take() { - callback(); - } - } -} - -fn poll_with(future: Pin<&mut F>, waker: &Waker) -> Poll { - future.poll(&mut Context::from_waker(waker)) -} - -fn assert_completes_without_deadlock(message: &'static str, test: impl FnOnce() + Send + 'static) { - let (finished_tx, finished_rx) = std::sync::mpsc::channel(); - let worker = thread::spawn(move || { - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(test)); - finished_tx.send(()).unwrap(); - if let Err(payload) = result { - std::panic::resume_unwind(payload); - } - }); - - finished_rx - .recv_timeout(Duration::from_secs(10)) - .expect(message); - worker.join().unwrap(); -} - #[test] fn all_observers_borrow_the_same_non_clone_value() { let (completer, completion) = completion::new(); @@ -182,8 +117,8 @@ fn completed_payload_is_released_with_the_last_observer() { fn abandonment_wakes_all_registered_waits() { let (completer, first) = completion::new::(); let second = first.clone(); - let first_tracker = Arc::new(TrackWake(AtomicUsize::new(0))); - let second_tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let first_tracker = Arc::new(WakeCounter::default()); + let second_tracker = Arc::new(WakeCounter::default()); let first_waker = Waker::from(first_tracker.clone()); let second_waker = Waker::from(second_tracker.clone()); let mut first_wait = Box::pin(first.wait()); @@ -193,8 +128,8 @@ fn abandonment_wakes_all_registered_waits() { assert!(poll_with(second_wait.as_mut(), &second_waker).is_pending()); drop(completer); - assert_eq!(first_tracker.0.load(Ordering::Relaxed), 1); - assert_eq!(second_tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(first_tracker.count(), 1); + assert_eq!(second_tracker.count(), 1); assert!(matches!( poll_with(first_wait.as_mut(), &first_waker), Poll::Ready(Err(_)) @@ -222,8 +157,8 @@ fn payload_errors_remain_distinct_from_abandonment() { #[test] fn cancelling_a_wait_releases_only_its_waker() { let (completer, completion) = completion::new(); - let cancelled_tracker = Arc::new(TrackWake(AtomicUsize::new(0))); - let waiting_tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let cancelled_tracker = Arc::new(WakeCounter::default()); + let waiting_tracker = Arc::new(WakeCounter::default()); let cancelled_waker = Waker::from(cancelled_tracker.clone()); let waiting_waker = Waker::from(waiting_tracker.clone()); let baseline = Arc::strong_count(&cancelled_tracker); @@ -237,8 +172,8 @@ fn cancelling_a_wait_releases_only_its_waker() { assert_eq!(Arc::strong_count(&cancelled_tracker), baseline); completer.complete(7).unwrap(); - assert_eq!(cancelled_tracker.0.load(Ordering::Relaxed), 0); - assert_eq!(waiting_tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(cancelled_tracker.count(), 0); + assert_eq!(waiting_tracker.count(), 1); assert_eq!( poll_with(waiting.as_mut(), &waiting_waker), Poll::Ready(Ok(&7)) @@ -249,13 +184,13 @@ fn cancelling_a_wait_releases_only_its_waker() { fn cancelling_after_wake_does_not_consume_the_shared_result() { let (completer, first) = completion::new(); let second = first.clone(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let mut wait = Box::pin(first.wait()); assert!(poll_with(wait.as_mut(), &waker).is_pending()); completer.complete(9).unwrap(); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); drop(wait); assert_eq!(pollster::block_on(second.wait()), Ok(&9)); @@ -264,7 +199,7 @@ fn cancelling_after_wake_does_not_consume_the_shared_result() { #[test] fn cancellation_and_completer_drop_have_clean_orderings() { let (completer, completion) = completion::new::(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let baseline = Arc::strong_count(&tracker); let mut wait = Box::pin(completion.wait()); @@ -274,18 +209,18 @@ fn cancellation_and_completer_drop_have_clean_orderings() { drop(wait); assert_eq!(Arc::strong_count(&tracker), baseline); drop(completer); - assert_eq!(tracker.0.load(Ordering::Relaxed), 0); + assert_eq!(tracker.count(), 0); assert!(pollster::block_on(completion.wait()).is_err()); let (completer, completion) = completion::new::(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let baseline = Arc::strong_count(&tracker); let mut wait = Box::pin(completion.wait()); assert!(poll_with(wait.as_mut(), &waker).is_pending()); drop(completer); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); assert_eq!(Arc::strong_count(&tracker), baseline); drop(wait); assert_eq!(Arc::strong_count(&tracker), baseline); @@ -297,7 +232,7 @@ fn completion_attempts_every_waker_after_one_panics() { let (completer, first) = completion::new(); let second = first.clone(); let panicking = Waker::from(Arc::new(PanicWake)); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let tracked = Waker::from(tracker.clone()); let mut first_wait = Box::pin(first.wait()); let mut second_wait = Box::pin(second.wait()); @@ -307,7 +242,7 @@ fn completion_attempts_every_waker_after_one_panics() { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| completer.complete(11))); assert!(result.is_err()); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); assert_eq!( poll_with(second_wait.as_mut(), &tracked), Poll::Ready(Ok(&11)) @@ -316,82 +251,65 @@ fn completion_attempts_every_waker_after_one_panics() { #[test] fn wake_callbacks_run_outside_the_completion_lock() { - assert_completes_without_deadlock( - "wake callback deadlocked against the completion lock", - || { - let (completer, completion) = completion::new(); - let callback_completion = completion.clone(); - let waker = Waker::from(Arc::new(WakeCallback(Mutex::new(Some(Box::new( - move || { - assert_eq!(pollster::block_on(callback_completion.wait()), Ok(&13)); - }, - )))))); - let mut wait = Box::pin(completion.wait()); - - assert!(poll_with(wait.as_mut(), &waker).is_pending()); - completer.complete(13).unwrap(); - assert_eq!(poll_with(wait.as_mut(), &waker), Poll::Ready(Ok(&13))); - drop(wait); - - let (completer, completion) = completion::new::(); - let callback_completion = completion.clone(); - let waker = Waker::from(Arc::new(WakeCallback(Mutex::new(Some(Box::new( - move || { - assert!(pollster::block_on(callback_completion.wait()).is_err()); - }, - )))))); - let mut wait = Box::pin(completion.wait()); - assert!(poll_with(wait.as_mut(), &waker).is_pending()); - drop(completer); - assert!(matches!( - poll_with(wait.as_mut(), &waker), - Poll::Ready(Err(_)) - )); - }, - ); + assert_completes_without_deadlock(|| { + let (completer, completion) = completion::new(); + let callback_completion = completion.clone(); + let waker = waker_on_wake(move || { + assert_eq!(pollster::block_on(callback_completion.wait()), Ok(&13)); + }); + let mut wait = Box::pin(completion.wait()); + + assert!(poll_with(wait.as_mut(), &waker).is_pending()); + completer.complete(13).unwrap(); + assert_eq!(poll_with(wait.as_mut(), &waker), Poll::Ready(Ok(&13))); + drop(wait); + + let (completer, completion) = completion::new::(); + let callback_completion = completion.clone(); + let waker = waker_on_wake(move || { + assert!(pollster::block_on(callback_completion.wait()).is_err()); + }); + let mut wait = Box::pin(completion.wait()); + assert!(poll_with(wait.as_mut(), &waker).is_pending()); + drop(completer); + assert!(matches!( + poll_with(wait.as_mut(), &waker), + Poll::Ready(Err(_)) + )); + }); } #[test] fn replaced_wakers_are_dropped_outside_the_completion_lock() { - assert_completes_without_deadlock( - "replaced waker destructor deadlocked against the completion lock", - || { - let (completer, completion) = completion::new::(); - let old_waker = Waker::from(Arc::new(DropCallbackWake(Mutex::new(Some(Box::new( - move || drop(completer), - )))))); - let mut wait = Box::pin(completion.wait()); - assert!(poll_with(wait.as_mut(), &old_waker).is_pending()); - drop(old_waker); - - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); - let replacement = Waker::from(tracker.clone()); - assert!(poll_with(wait.as_mut(), &replacement).is_pending()); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); - assert!(matches!( - poll_with(wait.as_mut(), &replacement), - Poll::Ready(Err(_)) - )); - }, - ); + assert_completes_without_deadlock(|| { + let (completer, completion) = completion::new::(); + let old_waker = waker_on_drop(move || drop(completer)); + let mut wait = Box::pin(completion.wait()); + assert!(poll_with(wait.as_mut(), &old_waker).is_pending()); + drop(old_waker); + + let tracker = Arc::new(WakeCounter::default()); + let replacement = Waker::from(tracker.clone()); + assert!(poll_with(wait.as_mut(), &replacement).is_pending()); + assert_eq!(tracker.count(), 1); + assert!(matches!( + poll_with(wait.as_mut(), &replacement), + Poll::Ready(Err(_)) + )); + }); } #[test] fn cancelled_wakers_are_dropped_outside_the_completion_lock() { - assert_completes_without_deadlock( - "cancelled waker destructor deadlocked against the completion lock", - || { - let (completer, completion) = completion::new::(); - let waker = Waker::from(Arc::new(DropCallbackWake(Mutex::new(Some(Box::new( - move || drop(completer), - )))))); - let mut wait = Box::pin(completion.wait()); - assert!(poll_with(wait.as_mut(), &waker).is_pending()); - drop(waker); - drop(wait); - assert!(pollster::block_on(completion.wait()).is_err()); - }, - ); + assert_completes_without_deadlock(|| { + let (completer, completion) = completion::new::(); + let waker = waker_on_drop(move || drop(completer)); + let mut wait = Box::pin(completion.wait()); + assert!(poll_with(wait.as_mut(), &waker).is_pending()); + drop(waker); + drop(wait); + assert!(pollster::block_on(completion.wait()).is_err()); + }); } #[test] diff --git a/tests-integration/tests/condvar_test.rs b/tests-integration/tests/condvar_test.rs index ffd1dd0..2f95bf2 100644 --- a/tests-integration/tests/condvar_test.rs +++ b/tests-integration/tests/condvar_test.rs @@ -15,33 +15,19 @@ // specific language governing permissions and limitations // under the License. -use std::future::Future; -use std::pin::Pin; use std::sync::Arc; -use std::task::Context; -use std::task::Poll; use std::task::Wake; use std::task::Waker; -use std::thread; -use std::time::Duration; use asyncband::condvar::Condvar; use asyncband::mutex::Mutex; +use tests_integration::assert_completes_without_deadlock; +use tests_integration::expect_ready; use tests_integration::poll_once; +use tests_integration::poll_with; use tests_integration::test_runtime; use tokio::task::JoinHandle; -fn expect_ready(poll: Poll) -> T { - match poll { - Poll::Ready(value) => value, - Poll::Pending => panic!("future should be ready"), - } -} - -fn poll_with(future: Pin<&mut F>, waker: &Waker) -> Poll { - future.poll(&mut Context::from_waker(waker)) -} - struct NotifyOnDrop(Arc); impl Wake for NotifyOnDrop { @@ -177,8 +163,7 @@ fn notify_all_wakes_current_waiters_using_a_predicate_loop() { #[test] fn cancelling_waiter_drops_its_waker_outside_the_waiter_lock() { - let (finished_tx, finished_rx) = std::sync::mpsc::channel(); - let worker = thread::spawn(move || { + assert_completes_without_deadlock(|| { let mutex = Mutex::new(()); let condvar = Arc::new(Condvar::new()); let waker = Waker::from(Arc::new(NotifyOnDrop(condvar.clone()))); @@ -187,13 +172,7 @@ fn cancelling_waiter_drops_its_waker_outside_the_waiter_lock() { assert!(poll_with(wait.as_mut(), &waker).is_pending()); drop(waker); drop(wait); - finished_tx.send(()).unwrap(); }); - - finished_rx - .recv_timeout(Duration::from_secs(10)) - .expect("dropping a cancelled waiter deadlocked against the condvar waiter lock"); - worker.join().unwrap(); } #[test] diff --git a/tests-integration/tests/event_test.rs b/tests-integration/tests/event_test.rs index 87ed2bd..122a851 100644 --- a/tests-integration/tests/event_test.rs +++ b/tests-integration/tests/event_test.rs @@ -23,26 +23,17 @@ use std::pin::pin; use std::sync::Arc; use std::sync::Mutex; use std::sync::atomic::AtomicBool; -use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; -use std::sync::mpsc; use std::task::Context; use std::task::Wake; use std::task::Waker; -use std::thread; -use std::time::Duration; use asyncband::event::ManualResetEvent; +use tests_integration::PanicWake; +use tests_integration::WakeCounter; +use tests_integration::assert_completes_without_deadlock; use tests_integration::poll_once; -struct TrackWake(AtomicUsize); - -impl Wake for TrackWake { - fn wake(self: Arc) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} - #[test] fn set_is_sticky_and_reset_blocks_new_waiters() { let event = ManualResetEvent::new(); @@ -71,8 +62,8 @@ fn an_unpolled_wait_is_not_a_waiter_of_a_preceding_set() { #[test] fn polling_with_a_new_waker_replaces_the_registration() { let event = ManualResetEvent::new(); - let first = Arc::new(TrackWake(AtomicUsize::new(0))); - let second = Arc::new(TrackWake(AtomicUsize::new(0))); + let first = Arc::new(WakeCounter::default()); + let second = Arc::new(WakeCounter::default()); let first_waker = Waker::from(first.clone()); let second_waker = Waker::from(second.clone()); let baseline = Arc::strong_count(&first); @@ -93,14 +84,14 @@ fn polling_with_a_new_waker_replaces_the_registration() { assert_eq!(Arc::strong_count(&first), baseline); event.set(); - assert_eq!(first.0.load(Ordering::Relaxed), 0); - assert_eq!(second.0.load(Ordering::Relaxed), 1); + assert_eq!(first.count(), 0); + assert_eq!(second.count(), 1); } #[test] fn cancelling_a_committed_waiter_leaves_the_others_committed() { let event = ManualResetEvent::new(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let mut context = Context::from_waker(&waker); let mut cancelled = Box::pin(event.wait()); @@ -111,7 +102,7 @@ fn cancelling_a_committed_waiter_leaves_the_others_committed() { event.set(); event.reset(); - assert_eq!(tracker.0.load(Ordering::Relaxed), 2); + assert_eq!(tracker.count(), 2); // A committed waiter holds no consumable permit, so dropping it hands nothing on. drop(cancelled); @@ -119,22 +110,14 @@ fn cancelling_a_committed_waiter_leaves_the_others_committed() { assert!(!event.is_set()); } -struct PanicOnWake; - -impl Wake for PanicOnWake { - fn wake(self: Arc) { - panic!("waker panicked"); - } -} - // `set` releases every waiter registered for the current unset period, so one broken waker must // not strand the waiters queued behind it. #[test] fn a_panicking_waker_still_releases_the_remaining_waiters() { let event = ManualResetEvent::new(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let tracking_waker = Waker::from(tracker.clone()); - let panicking_waker = Waker::from(Arc::new(PanicOnWake)); + let panicking_waker = Waker::from(Arc::new(PanicWake)); let mut first = Box::pin(event.wait()); let mut exploding = Box::pin(event.wait()); @@ -161,7 +144,7 @@ fn a_panicking_waker_still_releases_the_remaining_waiters() { assert!(panicked.is_err(), "the waker panic must reach the caller"); assert_eq!( - tracker.0.load(Ordering::Relaxed), + tracker.count(), 2, "the waiter queued behind the panicking waker was never woken" ); @@ -190,14 +173,9 @@ impl Drop for ReentrantWaker { } // Wakers must be invoked and dropped after the internal lock is released. -// -// The scenario runs on a worker thread so that a regression surfaces as a bounded failure here -// rather than hanging until the harness times out. #[test] fn wakers_are_woken_and_dropped_outside_the_internal_lock() { - let (done, finished) = mpsc::channel(); - - thread::spawn(move || { + assert_completes_without_deadlock(|| { let event = Arc::new(ManualResetEvent::new()); // `set` wakes the waiter, and the waker re-enters `reset`. @@ -245,13 +223,7 @@ fn wakers_are_woken_and_dropped_outside_the_internal_lock() { // Cancelling a pending wait drops the registered waker, which re-enters `is_set`. The // registration holds the last reference, so the drop runs here. drop(replaced); - - done.send(()).unwrap(); }); - - finished - .recv_timeout(Duration::from_secs(10)) - .expect("a waker was invoked or dropped while the internal lock was held"); } // A waker that resets the event and registers a fresh waiter from inside the wake callback. @@ -336,7 +308,7 @@ fn a_wait_registered_from_a_wake_callback_belongs_to_the_next_period() { #[test] fn set_then_reset_commits_registered_waiters() { let event = ManualResetEvent::new(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let mut context = Context::from_waker(&waker); let mut wait = pin!(event.wait()); @@ -345,7 +317,7 @@ fn set_then_reset_commits_registered_waiters() { event.set(); event.reset(); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); assert!(wait.as_mut().poll(&mut context).is_ready()); let mut next_generation = pin!(event.wait()); @@ -355,7 +327,7 @@ fn set_then_reset_commits_registered_waiters() { #[test] fn repeated_set_is_coalesced() { let event = ManualResetEvent::new(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let mut context = Context::from_waker(&waker); let mut wait = pin!(event.wait()); @@ -364,14 +336,14 @@ fn repeated_set_is_coalesced() { event.set(); event.set(); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); assert!(wait.as_mut().poll(&mut context).is_ready()); } #[test] fn cancelling_a_waiter_releases_its_waker() { let event = ManualResetEvent::new(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let baseline = Arc::strong_count(&tracker); let mut context = Context::from_waker(&waker); @@ -383,13 +355,13 @@ fn cancelling_a_waiter_releases_its_waker() { drop(wait); assert_eq!(Arc::strong_count(&tracker), baseline); event.set(); - assert_eq!(tracker.0.load(Ordering::Relaxed), 0); + assert_eq!(tracker.count(), 0); } #[test] fn cancelling_an_owned_waiter_releases_its_waker_and_event_handle() { let event = Arc::new(ManualResetEvent::new()); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let baseline = Arc::strong_count(&tracker); let mut context = Context::from_waker(&waker); @@ -403,5 +375,5 @@ fn cancelling_an_owned_waiter_releases_its_waker_and_event_handle() { assert_eq!(Arc::strong_count(&event), 1); event.set(); - assert_eq!(tracker.0.load(Ordering::Relaxed), 0); + assert_eq!(tracker.count(), 0); } diff --git a/tests-integration/tests/latch_test.rs b/tests-integration/tests/latch_test.rs index b527c0b..7a4e6c6 100644 --- a/tests-integration/tests/latch_test.rs +++ b/tests-integration/tests/latch_test.rs @@ -15,23 +15,12 @@ // specific language governing permissions and limitations // under the License. -use std::future::Future; use std::sync::Arc; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; use std::task::Context; -use std::task::Wake; use std::task::Waker; use asyncband::latch::Latch; - -struct TrackWake(AtomicUsize); - -impl Wake for TrackWake { - fn wake(self: Arc) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} +use tests_integration::WakeCounter; #[test] fn countdown_operations_saturate_at_zero() { @@ -55,7 +44,7 @@ fn countdown_operations_saturate_at_zero() { #[test] fn cancelled_wait_releases_its_waker() { let latch = Latch::new(1); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let baseline = Arc::strong_count(&tracker); let mut context = Context::from_waker(&waker); @@ -71,8 +60,8 @@ fn cancelled_wait_releases_its_waker() { #[test] fn final_arrival_wakes_every_waiter() { let latch = Latch::new(2); - let first_tracker = Arc::new(TrackWake(AtomicUsize::new(0))); - let second_tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let first_tracker = Arc::new(WakeCounter::default()); + let second_tracker = Arc::new(WakeCounter::default()); let first_waker = Waker::from(first_tracker.clone()); let second_waker = Waker::from(second_tracker.clone()); let mut first_context = Context::from_waker(&first_waker); @@ -84,12 +73,12 @@ fn final_arrival_wakes_every_waiter() { assert!(second.as_mut().poll(&mut second_context).is_pending()); latch.count_down(); - assert_eq!(first_tracker.0.load(Ordering::Relaxed), 0); - assert_eq!(second_tracker.0.load(Ordering::Relaxed), 0); + assert_eq!(first_tracker.count(), 0); + assert_eq!(second_tracker.count(), 0); latch.count_down(); - assert_eq!(first_tracker.0.load(Ordering::Relaxed), 1); - assert_eq!(second_tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(first_tracker.count(), 1); + assert_eq!(second_tracker.count(), 1); assert!(first.as_mut().poll(&mut first_context).is_ready()); assert!(second.as_mut().poll(&mut second_context).is_ready()); } diff --git a/tests-integration/tests/mpsc_test/backpressure.rs b/tests-integration/tests/mpsc_test/backpressure.rs index 2db9919..1ea57a7 100644 --- a/tests-integration/tests/mpsc_test/backpressure.rs +++ b/tests-integration/tests/mpsc_test/backpressure.rs @@ -16,11 +16,10 @@ // under the License. use asyncband::mpsc; +use tests_integration::WakeCounter; +use tests_integration::expect_ready; use tests_integration::poll_once; - -use super::support::WakeCounter; -use super::support::expect_ready; -use super::support::poll_with; +use tests_integration::poll_with; #[test] fn bounded_wakes_blocked_senders_one_at_a_time() { diff --git a/tests-integration/tests/mpsc_test/callbacks.rs b/tests-integration/tests/mpsc_test/callbacks.rs index a069f00..772c368 100644 --- a/tests-integration/tests/mpsc_test/callbacks.rs +++ b/tests-integration/tests/mpsc_test/callbacks.rs @@ -15,7 +15,6 @@ // specific language governing permissions and limitations // under the License. -use std::future::Future; use std::sync::Arc; use std::sync::Mutex; use std::sync::atomic::AtomicBool; @@ -28,14 +27,15 @@ use std::task::Waker; use asyncband::mpsc; use asyncband::mpsc::TryRecvError; +use tests_integration::WakeCounter; +use tests_integration::assert_completes_without_deadlock; +use tests_integration::expect_ready; use tests_integration::poll_once; +use tests_integration::poll_with; +use tests_integration::waker_on_drop; +use tests_integration::waker_on_wake; -use super::support::WakeCounter; -use super::support::assert_completes_without_deadlock; -use super::support::expect_ready; -use super::support::poll_with; use super::support::waker_on_clone; -use super::support::waker_on_drop; struct HoldSender { _sender: S, @@ -184,29 +184,22 @@ fn receive_rechecks_messages_sent_by_waker_clone() { #[test] fn wake_callbacks_can_send_into_the_same_channel() { - struct SendOnWake(Box); - impl Wake for SendOnWake { - fn wake(self: Arc) { - (self.0)(); - } - } - assert_completes_without_deadlock(|| { let (tx, mut rx) = mpsc::bounded(2); - let waker = Waker::from(Arc::new(SendOnWake(Box::new({ + let waker = waker_on_wake({ let tx = tx.clone(); move || tx.try_send(2).unwrap() - })))); + }); assert!(poll_with(Box::pin(rx.recv()).as_mut(), &waker).is_pending()); tx.try_send(1).unwrap(); assert_eq!(rx.try_recv(), Ok(1)); assert_eq!(rx.try_recv(), Ok(2)); let (tx, mut rx) = mpsc::unbounded(); - let waker = Waker::from(Arc::new(SendOnWake(Box::new({ + let waker = waker_on_wake({ let tx = tx.clone(); move || tx.send(2).unwrap() - })))); + }); assert!(poll_with(Box::pin(rx.recv()).as_mut(), &waker).is_pending()); tx.send(1).unwrap(); assert_eq!(rx.try_recv(), Ok(1)); diff --git a/tests-integration/tests/mpsc_test/concurrency.rs b/tests-integration/tests/mpsc_test/concurrency.rs index 788fbcb..d5c50a8 100644 --- a/tests-integration/tests/mpsc_test/concurrency.rs +++ b/tests-integration/tests/mpsc_test/concurrency.rs @@ -29,13 +29,12 @@ use asyncband::mpsc; use asyncband::mpsc::RecvError; use asyncband::mpsc::TryRecvError; use asyncband::mpsc::TrySendError; +use tests_integration::WakeCounter; use tests_integration::poll_once; +use tests_integration::poll_with; use tests_integration::test_runtime; use tokio_test::assert_ok; -use super::support::WakeCounter; -use super::support::poll_with; - #[test] fn publication_racing_with_close_drops_every_payload_once() { #[derive(Debug)] diff --git a/tests-integration/tests/mpsc_test/main.rs b/tests-integration/tests/mpsc_test/main.rs index a8c1378..057c792 100644 --- a/tests-integration/tests/mpsc_test/main.rs +++ b/tests-integration/tests/mpsc_test/main.rs @@ -21,11 +21,10 @@ use asyncband::mpsc; use asyncband::mpsc::RecvError; use asyncband::mpsc::TryRecvError; use asyncband::mpsc::TrySendError; +use tests_integration::WakeCounter; +use tests_integration::expect_ready; use tests_integration::poll_once; - -use self::support::WakeCounter; -use self::support::expect_ready; -use self::support::poll_with; +use tests_integration::poll_with; // Public channel contracts. The other suites cover backpressure, callbacks, and concurrency. mod backpressure; diff --git a/tests-integration/tests/mpsc_test/reservation.rs b/tests-integration/tests/mpsc_test/reservation.rs index 7d2a074..86ab54e 100644 --- a/tests-integration/tests/mpsc_test/reservation.rs +++ b/tests-integration/tests/mpsc_test/reservation.rs @@ -20,17 +20,16 @@ use std::mem; use std::panic::AssertUnwindSafe; use std::panic::catch_unwind; use std::sync::Arc; -use std::task::Wake; use std::task::Waker; use asyncband::mpsc; use asyncband::mpsc::TryRecvError; use asyncband::mpsc::TrySendError; +use tests_integration::PanicWake; +use tests_integration::WakeCounter; +use tests_integration::expect_ready; use tests_integration::poll_once; - -use super::support::WakeCounter; -use super::support::expect_ready; -use super::support::poll_with; +use tests_integration::poll_with; #[test] fn held_permits_consume_capacity_without_claiming_message_order() { @@ -153,15 +152,9 @@ fn a_permit_can_publish_send_only_payloads_from_another_thread() { #[test] fn a_panicking_publication_wake_cannot_return_capacity_twice() { - struct PanicOnWake; - impl Wake for PanicOnWake { - fn wake(self: Arc) { - panic!("publication wake"); - } - } let (tx, mut rx) = mpsc::bounded(1); let permit = tx.try_reserve().unwrap(); - let waker = Waker::from(Arc::new(PanicOnWake)); + let waker = Waker::from(Arc::new(PanicWake)); let mut receive = Box::pin(rx.recv()); assert!(poll_with(receive.as_mut(), &waker).is_pending()); assert!(catch_unwind(AssertUnwindSafe(|| permit.send(1))).is_err()); diff --git a/tests-integration/tests/mpsc_test/support.rs b/tests-integration/tests/mpsc_test/support.rs index fdeb836..9b0340b 100644 --- a/tests-integration/tests/mpsc_test/support.rs +++ b/tests-integration/tests/mpsc_test/support.rs @@ -15,71 +15,10 @@ // specific language governing permissions and limitations // under the License. -use std::future::Future; -use std::pin::Pin; use std::sync::Arc; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; -use std::task::Context; -use std::task::Poll; use std::task::RawWaker; use std::task::RawWakerVTable; -use std::task::Wake; use std::task::Waker; -use std::thread; - -pub fn expect_ready(poll: Poll) -> T { - match poll { - Poll::Ready(value) => value, - Poll::Pending => panic!("future should be ready"), - } -} - -#[derive(Default)] -pub struct WakeCounter(AtomicUsize); - -impl WakeCounter { - pub fn new() -> (Waker, Arc) { - let counter = Arc::new(Self::default()); - (Waker::from(counter.clone()), counter) - } - - pub fn count(&self) -> usize { - self.0.load(Ordering::Relaxed) - } -} - -impl Wake for WakeCounter { - fn wake(self: Arc) { - self.0.fetch_add(1, Ordering::Relaxed); - } - - fn wake_by_ref(self: &Arc) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} - -pub fn poll_with(future: Pin<&mut F>, waker: &Waker) -> Poll { - future.poll(&mut Context::from_waker(waker)) -} - -pub fn waker_on_drop(callback: impl Fn() + Send + Sync + 'static) -> Waker { - struct OnDrop(Box); - - // Only destruction runs the callback; waking consumes the reference as usual. - #[allow(clippy::manual_noop_waker)] - impl Wake for OnDrop { - fn wake(self: Arc) {} - } - - impl Drop for OnDrop { - fn drop(&mut self) { - (self.0)(); - } - } - - Waker::from(Arc::new(OnDrop(Box::new(callback)))) -} // RawWaker is needed only to exercise clone callbacks, which the safe Wake trait cannot override. pub fn waker_on_clone(callback: impl Fn() + Send + Sync + 'static) -> Waker { @@ -106,19 +45,3 @@ pub fn waker_on_clone(callback: impl Fn() + Send + Sync + 'static) -> Waker { // preserve that ownership. wake_by_ref borrows the reference without changing it. unsafe { Waker::from_raw(RawWaker::new(pointer, &VTABLE)) } } - -pub fn assert_completes_without_deadlock(test: impl FnOnce() + Send + 'static) { - let (finished_tx, finished_rx) = std::sync::mpsc::channel(); - let worker = thread::spawn(move || { - test(); - finished_tx.send(()).unwrap(); - }); - #[cfg(not(miri))] - finished_rx - .recv_timeout(std::time::Duration::from_secs(10)) - .expect("waker callback did not finish"); - // Miri detects deadlock itself; its interpretation time must not determine test success. - #[cfg(miri)] - finished_rx.recv().expect("waker callback did not finish"); - worker.join().unwrap(); -} diff --git a/tests-integration/tests/pool_acquire_cancelled_test.rs b/tests-integration/tests/pool_acquire_cancelled_test.rs index 9f8cffe..2f34dda 100644 --- a/tests-integration/tests/pool_acquire_cancelled_test.rs +++ b/tests-integration/tests/pool_acquire_cancelled_test.rs @@ -31,6 +31,7 @@ use asyncband::pool::ManageObject; use asyncband::pool::ObjectStatus; use asyncband::pool::bounded::Pool; use asyncband::pool::bounded::PoolConfig; +use tests_integration::WakeCounter; struct Manager { create_calls: Arc, @@ -63,25 +64,18 @@ impl ManageObject for Manager { } } -#[derive(Default)] -struct WakeCount(AtomicUsize); - -impl Wake for WakeCount { - fn wake(self: Arc) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} - fn ready(future: impl Future>) -> T { - ready_after_wake(future, &Arc::new(WakeCount(AtomicUsize::new(1)))) + let wakes = Arc::new(WakeCounter::default()); + wakes.wake_by_ref(); + ready_after_wake(future, &wakes) } -fn ready_after_wake(future: impl Future>, wakes: &Arc) -> T { +fn ready_after_wake(future: impl Future>, wakes: &Arc) -> T { let mut future = pin!(future); let waker = Waker::from(wakes.clone()); // Allow cooperative yields, but never poll away a missing notification or spin forever. for _ in 0..32 { - assert!(wakes.0.swap(0, Ordering::Relaxed) > 0, "missing wake"); + assert!(wakes.take() > 0, "missing wake"); match future.as_mut().poll(&mut Context::from_waker(&waker)) { Poll::Ready(Ok(value)) => return value, Poll::Ready(Err(())) => panic!("operation should succeed"), @@ -101,8 +95,8 @@ fn cancel_waiter(return_before_cancel: bool) { }, ); let held = ready(pool.get()); - let mut first_wakes = Arc::new(WakeCount::default()); - let mut next_wakes = Arc::new(WakeCount::default()); + let mut first_wakes = Arc::new(WakeCounter::default()); + let mut next_wakes = Arc::new(WakeCounter::default()); let first_waker = Waker::from(first_wakes.clone()); let next_waker = Waker::from(next_wakes.clone()); let mut first = Box::pin(pool.get()); @@ -123,11 +117,11 @@ fn cancel_waiter(return_before_cancel: bool) { if return_before_cancel { drop(held); // Cancel a notified waiter without requiring FIFO notification order. - if first_wakes.0.load(Ordering::Relaxed) == 0 { + if first_wakes.count() == 0 { std::mem::swap(&mut first, &mut next); std::mem::swap(&mut first_wakes, &mut next_wakes); } - assert!(first_wakes.0.load(Ordering::Relaxed) > 0); + assert!(first_wakes.count() > 0); // Cancel the notified future without polling it to retrieve its permit. drop(first); } else { @@ -177,7 +171,7 @@ fn cancelling_create_restores_capacity_for_waiting_get() { assert_eq!(create_calls.load(Ordering::Relaxed), 1); assert_eq!(pool.status().current_size, 0); - let wakes = Arc::new(WakeCount::default()); + let wakes = Arc::new(WakeCounter::default()); let waker = Waker::from(wakes.clone()); let mut next = Box::pin(pool.get()); assert!( @@ -187,7 +181,7 @@ fn cancelling_create_restores_capacity_for_waiting_get() { ); assert_eq!(create_calls.load(Ordering::Relaxed), 1); drop(creating); - assert!(wakes.0.load(Ordering::Relaxed) > 0); + assert!(wakes.count() > 0); assert_eq!(pool.status().current_size, 0); assert_eq!(pool.status().idle_count, 0); diff --git a/tests-integration/tests/semaphore_test.rs b/tests-integration/tests/semaphore_test.rs index 395cf6b..4c46bd5 100644 --- a/tests-integration/tests/semaphore_test.rs +++ b/tests-integration/tests/semaphore_test.rs @@ -15,17 +15,16 @@ // specific language governing permissions and limitations // under the License. -use std::future::Future; use std::pin::pin; use std::sync::Arc; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; use std::task::Context; use std::task::Poll; use std::task::Wake; use std::task::Waker; use asyncband::semaphore::Semaphore; +use tests_integration::PanicWake; +use tests_integration::WakeCounter; #[test] fn no_permits() { @@ -201,27 +200,11 @@ fn wake_then_drop() { #[test] fn release_attempts_every_waker_after_one_panics() { - struct PanicOnWake; - - impl Wake for PanicOnWake { - fn wake(self: Arc) { - panic!("waker panicked"); - } - } - - struct CountWakes(AtomicUsize); - - impl Wake for CountWakes { - fn wake(self: Arc) { - self.0.fetch_add(1, Ordering::Relaxed); - } - } - let semaphore = Semaphore::new(0); let mut panicking = pin!(semaphore.acquire(1)); let mut tracked = pin!(semaphore.acquire(1)); - let panic_waker = Waker::from(Arc::new(PanicOnWake)); - let wake_count = Arc::new(CountWakes(AtomicUsize::new(0))); + let panic_waker = Waker::from(Arc::new(PanicWake)); + let wake_count = Arc::new(WakeCounter::default()); let tracked_waker = Waker::from(wake_count.clone()); assert!( @@ -240,7 +223,7 @@ fn release_attempts_every_waker_after_one_panics() { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| semaphore.release(2))); assert!(result.is_err()); - assert_eq!(wake_count.0.load(Ordering::Relaxed), 1); + assert_eq!(wake_count.count(), 1); assert!( panicking .as_mut() diff --git a/tests-integration/tests/unsafe_paths_test.rs b/tests-integration/tests/unsafe_paths_test.rs index 044492c..3a7ace5 100644 --- a/tests-integration/tests/unsafe_paths_test.rs +++ b/tests-integration/tests/unsafe_paths_test.rs @@ -24,7 +24,6 @@ use std::ptr; use std::sync::Arc; use std::task::Context; use std::task::Poll; -use std::task::Waker; use asyncband::mutex::MappedMutexGuard; use asyncband::mutex::Mutex; @@ -41,10 +40,7 @@ use asyncband::rwlock::OwnedRwLockWriteGuard; use asyncband::rwlock::RwLock; use asyncband::rwlock::RwLockReadGuard; use asyncband::rwlock::RwLockWriteGuard; - -fn poll_once(future: Pin<&mut F>) -> Poll { - future.poll(&mut Context::from_waker(Waker::noop())) -} +use tests_integration::poll_once; #[test] fn mapped_mutex_guards_preserve_lock_ownership() { diff --git a/tests-integration/tests/waitgroup_test.rs b/tests-integration/tests/waitgroup_test.rs index 1893284..c78c6b4 100644 --- a/tests-integration/tests/waitgroup_test.rs +++ b/tests-integration/tests/waitgroup_test.rs @@ -15,26 +15,14 @@ // specific language governing permissions and limitations // under the License. -use std::future::Future; -use std::future::IntoFuture; use std::sync::Arc; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; use std::task::Context; -use std::task::Wake; use std::task::Waker; use asyncband::waitgroup::WaitGroup; +use tests_integration::WakeCounter; use tests_integration::poll_once; -struct WakeCount(AtomicUsize); - -impl Wake for WakeCount { - fn wake(self: Arc) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn waits_for_all_worker_handles() { let wg = WaitGroup::new(); @@ -130,8 +118,8 @@ fn repolling_replaces_the_registered_waker() { let wg = WaitGroup::new(); let worker = wg.clone(); let mut wait = Box::pin(wg.into_future()); - let first = Arc::new(WakeCount(AtomicUsize::new(0))); - let second = Arc::new(WakeCount(AtomicUsize::new(0))); + let first = Arc::new(WakeCounter::default()); + let second = Arc::new(WakeCounter::default()); let first_waker = Waker::from(first.clone()); let second_waker = Waker::from(second.clone()); @@ -147,6 +135,6 @@ fn repolling_replaces_the_registered_waker() { ); drop(worker); - assert_eq!(first.0.load(Ordering::Relaxed), 0); - assert_eq!(second.0.load(Ordering::Relaxed), 1); + assert_eq!(first.count(), 0); + assert_eq!(second.count(), 1); } diff --git a/tests-integration/tests/watch_test.rs b/tests-integration/tests/watch_test.rs index ccc15f7..72d8ddb 100644 --- a/tests-integration/tests/watch_test.rs +++ b/tests-integration/tests/watch_test.rs @@ -15,64 +15,19 @@ // specific language governing permissions and limitations // under the License. -use std::future::Future; -use std::pin::Pin; use std::sync::Arc; -use std::sync::Mutex; use std::sync::atomic::AtomicBool; -use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; -use std::task::Context; use std::task::Poll; -use std::task::Wake; use std::task::Waker; -use std::thread; -use std::time::Duration; use asyncband::watch; - -struct TrackWake(AtomicUsize); - -impl Wake for TrackWake { - fn wake(self: Arc) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} - -struct PanicWake; - -impl Wake for PanicWake { - fn wake(self: Arc) { - panic!("wake failed"); - } -} - -struct WakeCallback(Mutex>>); - -impl Wake for WakeCallback { - fn wake(self: Arc) { - let callback = self.0.lock().unwrap().take(); - if let Some(callback) = callback { - callback(); - } - } -} - -struct DropCallbackWake(Mutex>>); - -// This test needs a custom waker whose final `Arc` drop is observable. -#[allow(clippy::manual_noop_waker)] -impl Wake for DropCallbackWake { - fn wake(self: Arc) {} -} - -impl Drop for DropCallbackWake { - fn drop(&mut self) { - if let Some(callback) = self.0.get_mut().unwrap().take() { - callback(); - } - } -} +use tests_integration::PanicWake; +use tests_integration::WakeCounter; +use tests_integration::assert_completes_without_deadlock; +use tests_integration::poll_with; +use tests_integration::waker_on_drop; +use tests_integration::waker_on_wake; #[derive(Clone)] struct ReentrantDrop(Option>); @@ -103,10 +58,6 @@ impl Clone for PanicOnceClone { } } -fn poll_with(future: Pin<&mut F>, waker: &Waker) -> Poll { - future.poll(&mut Context::from_waker(waker)) -} - #[test] fn initial_value_is_observed_and_updates_coalesce() { let (tx, mut rx) = watch::channel(0); @@ -254,7 +205,7 @@ fn send_replace_returns_previous_and_publishes_without_receivers() { #[test] fn cancelling_changed_releases_its_waker_without_consuming() { let (tx, mut rx) = watch::channel(0); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let baseline = Arc::strong_count(&tracker); let mut changed = Box::pin(rx.changed()); @@ -265,20 +216,20 @@ fn cancelling_changed_releases_its_waker_without_consuming() { assert_eq!(Arc::strong_count(&tracker), baseline); tx.send(1).unwrap(); - assert_eq!(tracker.0.load(Ordering::Relaxed), 0); + assert_eq!(tracker.count(), 0); assert_eq!(pollster::block_on(rx.recv()).unwrap(), 1); } #[test] fn cancelling_after_wake_still_leaves_the_update_unseen() { let (tx, mut rx) = watch::channel(0); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let mut changed = Box::pin(rx.changed()); assert!(poll_with(changed.as_mut(), &waker).is_pending()); tx.send(1).unwrap(); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); drop(changed); assert_eq!(pollster::block_on(rx.recv()).unwrap(), 1); @@ -287,13 +238,13 @@ fn cancelling_after_wake_still_leaves_the_update_unseen() { #[test] fn cancelling_recv_after_wake_still_leaves_the_update_unseen() { let (tx, mut rx) = watch::channel(0); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let mut recv = Box::pin(rx.recv()); assert!(poll_with(recv.as_mut(), &waker).is_pending()); tx.send(1).unwrap(); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); drop(recv); assert_eq!(pollster::block_on(rx.recv()).unwrap(), 1); @@ -303,8 +254,8 @@ fn cancelling_recv_after_wake_still_leaves_the_update_unseen() { fn one_update_wakes_every_waiting_receiver_once() { let (tx, mut first) = watch::channel(0); let mut second = first.clone(); - let first_tracker = Arc::new(TrackWake(AtomicUsize::new(0))); - let second_tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let first_tracker = Arc::new(WakeCounter::default()); + let second_tracker = Arc::new(WakeCounter::default()); let first_waker = Waker::from(first_tracker.clone()); let second_waker = Waker::from(second_tracker.clone()); let mut first_changed = Box::pin(first.changed()); @@ -314,8 +265,8 @@ fn one_update_wakes_every_waiting_receiver_once() { assert!(poll_with(second_changed.as_mut(), &second_waker).is_pending()); tx.send(1).unwrap(); - assert_eq!(first_tracker.0.load(Ordering::Relaxed), 1); - assert_eq!(second_tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(first_tracker.count(), 1); + assert_eq!(second_tracker.count(), 1); assert_eq!( poll_with(first_changed.as_mut(), &first_waker), Poll::Ready(Ok(())) @@ -335,7 +286,7 @@ fn panicking_waker_does_not_skip_other_waiters() { let (tx, mut first) = watch::channel(0); let mut second = first.clone(); let panicking = Waker::from(Arc::new(PanicWake)); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let tracked = Waker::from(tracker.clone()); let mut first_changed = Box::pin(first.changed()); let mut second_changed = Box::pin(second.changed()); @@ -345,7 +296,7 @@ fn panicking_waker_does_not_skip_other_waiters() { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| tx.send(1))); assert!(result.is_err()); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); assert_eq!( poll_with(second_changed.as_mut(), &tracked), Poll::Ready(Ok(())) @@ -356,15 +307,15 @@ fn panicking_waker_does_not_skip_other_waiters() { fn only_the_last_sender_drop_wakes_a_waiter() { let (tx, mut rx) = watch::channel(()); let other = tx.clone(); - let tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let tracker = Arc::new(WakeCounter::default()); let waker = Waker::from(tracker.clone()); let mut changed = Box::pin(rx.changed()); assert!(poll_with(changed.as_mut(), &waker).is_pending()); drop(tx); - assert_eq!(tracker.0.load(Ordering::Relaxed), 0); + assert_eq!(tracker.count(), 0); drop(other); - assert_eq!(tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(tracker.count(), 1); assert_eq!( poll_with(changed.as_mut(), &waker), Poll::Ready(Err(watch::RecvError::Disconnected)) @@ -375,16 +326,16 @@ fn only_the_last_sender_drop_wakes_a_waiter() { fn dropping_a_stale_changed_future_keeps_a_new_waiter_registered() { let (tx, mut first) = watch::channel(0); let mut second = first.clone(); - let first_tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let first_tracker = Arc::new(WakeCounter::default()); let first_waker = Waker::from(first_tracker.clone()); let mut first_changed = Box::pin(first.changed()); assert!(poll_with(first_changed.as_mut(), &first_waker).is_pending()); tx.send(1).unwrap(); - assert_eq!(first_tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(first_tracker.count(), 1); assert_eq!(pollster::block_on(second.recv()).unwrap(), 1); - let second_tracker = Arc::new(TrackWake(AtomicUsize::new(0))); + let second_tracker = Arc::new(WakeCounter::default()); let second_waker = Waker::from(second_tracker.clone()); let mut second_changed = Box::pin(second.changed()); assert!(poll_with(second_changed.as_mut(), &second_waker).is_pending()); @@ -392,7 +343,7 @@ fn dropping_a_stale_changed_future_keeps_a_new_waiter_registered() { drop(first_changed); tx.send(2).unwrap(); - assert_eq!(second_tracker.0.load(Ordering::Relaxed), 1); + assert_eq!(second_tracker.count(), 1); assert_eq!( poll_with(second_changed.as_mut(), &second_waker), Poll::Ready(Ok(())) @@ -401,31 +352,21 @@ fn dropping_a_stale_changed_future_keeps_a_new_waiter_registered() { #[test] fn replaced_values_are_dropped_outside_the_channel_lock() { - let (finished_tx, finished_rx) = std::sync::mpsc::channel(); - let worker = thread::spawn(move || { + assert_completes_without_deadlock(|| { let (tx, _rx) = watch::channel(ReentrantDrop(None)); tx.send(ReentrantDrop(Some(tx.clone()))).unwrap(); tx.send(ReentrantDrop(None)).unwrap(); - finished_tx.send(()).unwrap(); }); - - finished_rx - .recv_timeout(Duration::from_secs(10)) - .expect("value destructor deadlocked against the watch lock"); - worker.join().unwrap(); } #[test] fn wake_callbacks_run_outside_the_channel_lock() { - let (finished_tx, finished_rx) = std::sync::mpsc::channel(); - let worker = thread::spawn(move || { + assert_completes_without_deadlock(|| { let (tx, mut rx) = watch::channel(0); let callback_sender = tx.clone(); - let waker = Waker::from(Arc::new(WakeCallback(Mutex::new(Some(Box::new( - move || { - drop(callback_sender.subscribe()); - }, - )))))); + let waker = waker_on_wake(move || { + drop(callback_sender.subscribe()); + }); let mut changed = Box::pin(rx.changed()); assert!(poll_with(changed.as_mut(), &waker).is_pending()); @@ -434,11 +375,9 @@ fn wake_callbacks_run_outside_the_channel_lock() { drop(changed); let observer = rx.clone(); - let waker = Waker::from(Arc::new(WakeCallback(Mutex::new(Some(Box::new( - move || { - assert!(observer.is_disconnected()); - }, - )))))); + let waker = waker_on_wake(move || { + assert!(observer.is_disconnected()); + }); let mut changed = Box::pin(rx.changed()); assert!(poll_with(changed.as_mut(), &waker).is_pending()); drop(tx); @@ -446,37 +385,22 @@ fn wake_callbacks_run_outside_the_channel_lock() { poll_with(changed.as_mut(), &waker), Poll::Ready(Err(watch::RecvError::Disconnected)) ); - finished_tx.send(()).unwrap(); }); - - finished_rx - .recv_timeout(Duration::from_secs(10)) - .expect("wake callback deadlocked against the watch lock"); - worker.join().unwrap(); } #[test] fn replaced_wakers_are_dropped_outside_the_channel_lock() { - let (finished_tx, finished_rx) = std::sync::mpsc::channel(); - let worker = thread::spawn(move || { + assert_completes_without_deadlock(|| { let (tx, mut rx) = watch::channel(0); let callback_sender = tx.clone(); - let old_waker = Waker::from(Arc::new(DropCallbackWake(Mutex::new(Some(Box::new( - move || { - drop(callback_sender.subscribe()); - }, - )))))); + let old_waker = waker_on_drop(move || { + drop(callback_sender.subscribe()); + }); let mut changed = Box::pin(rx.changed()); assert!(poll_with(changed.as_mut(), &old_waker).is_pending()); drop(old_waker); - let replacement = Waker::from(Arc::new(TrackWake(AtomicUsize::new(0)))); + let replacement = Waker::from(Arc::new(WakeCounter::default())); assert!(poll_with(changed.as_mut(), &replacement).is_pending()); - finished_tx.send(()).unwrap(); }); - - finished_rx - .recv_timeout(Duration::from_secs(10)) - .expect("replaced waker destructor deadlocked against the watch lock"); - worker.join().unwrap(); } From 469bbe8fc90743724f8e32b5895987e93200f40f Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:41:17 +0800 Subject: [PATCH 13/26] bench(watch): keep one shared comparison suite Signed-off-by: tison --- benchmarks/asyncband/main.rs | 1 - benchmarks/asyncband/watch/mod.rs | 101 ---------------------------- benchmarks/ecosystem/watch/paths.rs | 17 ++++- 3 files changed, 16 insertions(+), 103 deletions(-) delete mode 100644 benchmarks/asyncband/watch/mod.rs diff --git a/benchmarks/asyncband/main.rs b/benchmarks/asyncband/main.rs index 53ae706..907a07e 100644 --- a/benchmarks/asyncband/main.rs +++ b/benchmarks/asyncband/main.rs @@ -34,7 +34,6 @@ mod shutdown; mod singleflight; mod support; mod waitgroup; -mod watch; fn main() { divan::main(); diff --git a/benchmarks/asyncband/watch/mod.rs b/benchmarks/asyncband/watch/mod.rs deleted file mode 100644 index 7ab2fd3..0000000 --- a/benchmarks/asyncband/watch/mod.rs +++ /dev/null @@ -1,101 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::pin::pin; - -use asyncband::watch; -use divan::Bencher; -use divan::black_box; - -use crate::support::bench_context; -use crate::support::poll_pending; -use crate::support::poll_pinned_ready; -use crate::support::poll_ready; - -const RECEIVER_COUNTS: &[usize] = &[1, 2, 4, 8, 32]; - -#[divan::bench] -fn get_current(bencher: Bencher) { - let (sender, receiver) = watch::channel(1usize); - bencher.bench_local(|| black_box(receiver.get())); - black_box(sender); -} - -#[divan::bench] -fn send_and_get(bencher: Bencher) { - let (sender, receiver) = watch::channel(0usize); - bencher.bench_local(|| { - sender.send(black_box(1usize)).unwrap(); - black_box(receiver.get()) - }); -} - -#[divan::bench] -fn ready_recv(bencher: Bencher) { - let mut context = bench_context(); - let (sender, mut receiver) = watch::channel(0usize); - bencher.bench_local(|| { - sender.send(black_box(1usize)).unwrap(); - black_box(poll_ready(receiver.recv(), &mut context).unwrap()) - }); -} - -#[divan::bench] -fn ready_changed(bencher: Bencher) { - let mut context = bench_context(); - let (sender, mut receiver) = watch::channel(0usize); - bencher.bench_local(|| { - sender.send(black_box(1usize)).unwrap(); - poll_ready(receiver.changed(), &mut context).unwrap(); - }); -} - -#[divan::bench] -fn notify_pending(bencher: Bencher) { - let mut context = bench_context(); - let (sender, mut receiver) = watch::channel(0usize); - bencher.bench_local(|| { - let mut changed = pin!(receiver.changed()); - poll_pending(changed.as_mut(), &mut context); - sender.send(black_box(1usize)).unwrap(); - poll_pinned_ready(changed.as_mut(), &mut context).unwrap(); - }); -} - -#[divan::bench(args = RECEIVER_COUNTS)] -fn notify_pending_fanout(bencher: Bencher, receiver_count: usize) { - let mut context = bench_context(); - let (sender, first) = watch::channel(0usize); - let mut receivers = Vec::with_capacity(receiver_count); - receivers.push(first); - receivers.extend((1..receiver_count).map(|_| sender.subscribe())); - - bencher.bench_local(|| { - let mut changed = receivers - .iter_mut() - .map(|receiver| Box::pin(receiver.changed())) - .collect::>(); - for future in &mut changed { - poll_pending(future.as_mut(), &mut context); - } - - sender.send(black_box(1usize)).unwrap(); - for mut future in changed { - poll_pinned_ready(future.as_mut(), &mut context).unwrap(); - } - }); -} diff --git a/benchmarks/ecosystem/watch/paths.rs b/benchmarks/ecosystem/watch/paths.rs index f20d253..1f0bb50 100644 --- a/benchmarks/ecosystem/watch/paths.rs +++ b/benchmarks/ecosystem/watch/paths.rs @@ -20,6 +20,8 @@ // continuing. The recv adapter combines Tokio's changed and borrow_and_update operations to match // Asyncband's owned receive contract. +use std::pin::pin; + use divan::Bencher; use divan::black_box; @@ -31,7 +33,7 @@ use crate::support::poll_pending; use crate::support::poll_pinned_ready; use crate::support::poll_ready; -const RECEIVER_COUNTS: &[usize] = &[1, 2, 4, 8, 32]; +const RECEIVER_COUNTS: &[usize] = &[2, 4, 8, 32]; #[divan::bench(types = [Asyncband, Tokio])] fn get_current(bencher: Bencher) { @@ -73,6 +75,19 @@ fn ready_changed(bencher: Bencher) { }); } +#[divan::bench(types = [Asyncband, Tokio])] +fn notify_pending(bencher: Bencher) { + let mut context = bench_context(); + let (sender, mut receivers) = C::channel(1); + let mut receiver = receivers.pop().unwrap(); + bencher.bench_local(|| { + let mut changed = pin!(C::changed(&mut receiver)); + poll_pending(changed.as_mut(), &mut context); + C::send(&sender, black_box(1)); + poll_pinned_ready(changed.as_mut(), &mut context); + }); +} + #[divan::bench(types = [Asyncband, Tokio], args = RECEIVER_COUNTS)] fn notify_pending_fanout(bencher: Bencher, receiver_count: usize) { let mut context = bench_context(); From f23ab1b028ce5abfd4b0b618bae442a33ef235e7 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:41:20 +0800 Subject: [PATCH 14/26] bench(waitgroup): remove duplicated lifecycle cases Signed-off-by: tison --- benchmarks/asyncband/waitgroup/wait.rs | 64 +------------------------- 1 file changed, 2 insertions(+), 62 deletions(-) diff --git a/benchmarks/asyncband/waitgroup/wait.rs b/benchmarks/asyncband/waitgroup/wait.rs index 8affbeb..a02c5f4 100644 --- a/benchmarks/asyncband/waitgroup/wait.rs +++ b/benchmarks/asyncband/waitgroup/wait.rs @@ -16,7 +16,6 @@ // under the License. use std::future::IntoFuture; -use std::pin::pin; use asyncband::waitgroup::WaitGroup; use divan::Bencher; @@ -26,68 +25,9 @@ use crate::support::bench_context; use crate::support::poll_pending; use crate::support::poll_pinned_ready; -const WORKER_COUNTS: &[usize] = &[1, 8, 32]; +const WAITER_COUNTS: &[usize] = &[1, 8, 32]; -#[divan::bench] -fn ready_empty(bencher: Bencher) { - let mut context = bench_context(); - - bencher.bench_local(|| { - let mut wait = pin!(WaitGroup::new().into_future()); - poll_pinned_ready(wait.as_mut(), &mut context); - black_box(()) - }); -} - -#[divan::bench] -fn cancel_pending(bencher: Bencher) { - let mut context = bench_context(); - - bencher.bench_local(|| { - let root = WaitGroup::new(); - let worker = root.clone(); - { - let mut wait = pin!(root.into_future()); - poll_pending(wait.as_mut(), &mut context); - } - drop(worker); - black_box(()) - }); -} - -#[divan::bench] -fn complete_waiter(bencher: Bencher) { - let mut context = bench_context(); - - bencher.bench_local(|| { - let root = WaitGroup::new(); - let worker = root.clone(); - let mut wait = pin!(root.into_future()); - poll_pending(wait.as_mut(), &mut context); - - drop(worker); - poll_pinned_ready(wait.as_mut(), &mut context); - black_box(()) - }); -} - -#[divan::bench(args = WORKER_COUNTS)] -fn complete_worker_batch(bencher: Bencher, worker_count: usize) { - let mut context = bench_context(); - - bencher.bench_local(|| { - let root = WaitGroup::new(); - let workers = (0..worker_count).map(|_| root.clone()).collect::>(); - let mut wait = pin!(root.into_future()); - poll_pending(wait.as_mut(), &mut context); - - drop(workers); - poll_pinned_ready(wait.as_mut(), &mut context); - black_box(()) - }); -} - -#[divan::bench(args = WORKER_COUNTS)] +#[divan::bench(args = WAITER_COUNTS)] fn complete_waiter_batch(bencher: Bencher, waiter_count: usize) { let mut context = bench_context(); From 99a86be17e364d78c121d8c2eedbb503db9e4a6c Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:41:23 +0800 Subject: [PATCH 15/26] bench(broadcast): reuse ecosystem concurrency coverage Signed-off-by: tison --- .../asyncband/broadcast/mpmc/unbounded.rs | 165 ------------------ 1 file changed, 165 deletions(-) diff --git a/benchmarks/asyncband/broadcast/mpmc/unbounded.rs b/benchmarks/asyncband/broadcast/mpmc/unbounded.rs index fa35c27..d4fc08e 100644 --- a/benchmarks/asyncband/broadcast/mpmc/unbounded.rs +++ b/benchmarks/asyncband/broadcast/mpmc/unbounded.rs @@ -21,10 +21,6 @@ use std::fmt; use std::pin::pin; -use std::sync::Arc; -use std::sync::Barrier; -use std::thread; -use std::thread::JoinHandle; use asyncband::broadcast::mpmc; use divan::Bencher; @@ -35,8 +31,6 @@ use crate::support::poll_pending; use crate::support::poll_pinned_ready; const RECEIVER_COUNTS: &[usize] = &[1, 8, 32]; -const CONCURRENCY_COUNTS: &[usize] = &[1, 2, 4, 8]; -const CONCURRENT_BATCH_SIZE: usize = 4096; /// A channel that peaked at `peak` receivers and currently has `live` of them. /// @@ -69,131 +63,6 @@ const RECLAIM_FANOUTS: &[Fanout] = &[ Fanout { peak: 256, live: 1 }, ]; -struct ConcurrentSend { - receiver: mpmc::UnboundedReceiver, - start: Arc, - done: Arc, - workers: Vec>, -} - -impl ConcurrentSend { - fn new(sender_count: usize) -> Self { - let (sender, receiver) = mpmc::unbounded(); - let ready = Arc::new(Barrier::new(sender_count + 1)); - let start = Arc::new(Barrier::new(sender_count + 1)); - let done = Arc::new(Barrier::new(sender_count + 1)); - let sends_per_worker = CONCURRENT_BATCH_SIZE / sender_count; - let mut workers = Vec::with_capacity(sender_count); - - for worker_index in 0..sender_count { - let sender = sender.clone(); - let ready = ready.clone(); - let start = start.clone(); - let done = done.clone(); - workers.push(thread::spawn(move || { - ready.wait(); - start.wait(); - let first = worker_index * sends_per_worker; - for value in first..first + sends_per_worker { - sender.send(black_box(value)); - } - done.wait(); - })); - } - drop(sender); - ready.wait(); - - Self { - receiver, - start, - done, - workers, - } - } - - // The drain is inside the measured region on purpose: it is what keeps the backlog bounded - // across samples, and reclaiming the batch is part of the cost of an unbounded send. - fn run(&mut self) { - self.start.wait(); - self.done.wait(); - while let Ok(value) = self.receiver.try_recv() { - black_box(value); - } - } -} - -impl Drop for ConcurrentSend { - fn drop(&mut self) { - for worker in self.workers.drain(..) { - worker.join().unwrap(); - } - } -} - -struct ConcurrentFanout { - sender: mpmc::UnboundedSender, - start: Arc, - done: Arc, - workers: Vec>, -} - -impl ConcurrentFanout { - fn new(receiver_count: usize) -> Self { - let (sender, receiver) = mpmc::unbounded(); - let mut receivers = Vec::with_capacity(receiver_count); - receivers.push(receiver); - for _ in 1..receiver_count { - receivers.push(sender.subscribe()); - } - - let ready = Arc::new(Barrier::new(receiver_count + 1)); - let start = Arc::new(Barrier::new(receiver_count + 1)); - let done = Arc::new(Barrier::new(receiver_count + 1)); - let mut workers = Vec::with_capacity(receiver_count); - - for mut receiver in receivers { - let ready = ready.clone(); - let start = start.clone(); - let done = done.clone(); - workers.push(thread::spawn(move || { - ready.wait(); - start.wait(); - let result = (0..CONCURRENT_BATCH_SIZE).try_for_each(|_| { - receiver.try_recv().map(|value| { - black_box(value); - }) - }); - done.wait(); - result.unwrap(); - })); - } - ready.wait(); - - Self { - sender, - start, - done, - workers, - } - } - - fn run(&mut self) { - for value in 0..CONCURRENT_BATCH_SIZE { - self.sender.send(black_box(value)); - } - self.start.wait(); - self.done.wait(); - } -} - -impl Drop for ConcurrentFanout { - fn drop(&mut self) { - for worker in self.workers.drain(..) { - worker.join().unwrap(); - } - } -} - #[divan::bench] fn send_without_receivers(bencher: Bencher) { let (sender, receiver) = mpmc::unbounded::(); @@ -208,16 +77,6 @@ fn try_recv_empty(bencher: Bencher) { black_box(sender); } -// A sole receiver takes ownership of the payload, so this path never clones the message. -#[divan::bench] -fn send_and_try_recv(bencher: Bencher) { - let (sender, mut receiver) = mpmc::unbounded(); - bencher.bench_local(|| { - sender.send(black_box(1usize)); - black_box(receiver.try_recv().unwrap()) - }); -} - // With the payload shared, each receive clones it and the second one reclaims the slot. #[divan::bench] fn send_and_try_recv_shared(bencher: Bencher) { @@ -276,30 +135,6 @@ fn drain_with_receivers(bencher: Bencher, fanout: Fanout) { }); } -#[divan::bench( - args = CONCURRENCY_COUNTS, - sample_count = 50, - sample_size = 1, - counters = [CONCURRENT_BATCH_SIZE] -)] -fn concurrent_send_and_drain(bencher: Bencher, sender_count: usize) { - bencher - .with_inputs(|| ConcurrentSend::new(sender_count)) - .bench_local_refs(ConcurrentSend::run); -} - -#[divan::bench( - args = CONCURRENCY_COUNTS, - sample_count = 50, - sample_size = 1, - counters = [CONCURRENT_BATCH_SIZE] -)] -fn concurrent_fanout(bencher: Bencher, receiver_count: usize) { - bencher - .with_inputs(|| ConcurrentFanout::new(receiver_count)) - .bench_local_refs(ConcurrentFanout::run); -} - #[divan::bench] fn cancel_pending(bencher: Bencher) { let mut context = bench_context(); From 32d182003676d5ad75d53fcba98ed9e64b0e6d06 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:41:27 +0800 Subject: [PATCH 16/26] bench(mpsc): retain representative workload dimensions Signed-off-by: tison --- benchmarks/ecosystem/mpsc/bounded.rs | 50 +++------------------- benchmarks/ecosystem/mpsc/support.rs | 59 +------------------------- benchmarks/ecosystem/mpsc/unbounded.rs | 22 ++-------- 3 files changed, 12 insertions(+), 119 deletions(-) diff --git a/benchmarks/ecosystem/mpsc/bounded.rs b/benchmarks/ecosystem/mpsc/bounded.rs index 44d2cd0..3678f42 100644 --- a/benchmarks/ecosystem/mpsc/bounded.rs +++ b/benchmarks/ecosystem/mpsc/bounded.rs @@ -27,7 +27,6 @@ use super::adapters::Tokio; use super::support::BATCH_MESSAGES; use super::support::BOUNDED_CAPACITY; use super::support::Bounded; -use super::support::ConcurrentBatch; use super::support::PRODUCER_COUNTS; use super::support::RepeatedBatch; use super::support::RepeatedTasks; @@ -58,19 +57,6 @@ fn ready_round_trip(bencher: Bencher) { }); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = PRODUCER_COUNTS, - sample_count = 20, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] -fn concurrent(bencher: Bencher, producer_count: usize) { - bencher - .with_inputs(|| ConcurrentBatch::>::new(producer_count)) - .bench_local_refs(|batch| batch.run()); -} - #[divan::bench( types = [Asyncband, Tokio, AsyncChannel, Flume], args = PRODUCER_COUNTS, @@ -92,24 +78,7 @@ fn clone_drop_sender(bencher: Bencher) { #[divan::bench( types = [Asyncband, Tokio, AsyncChannel, Flume], - consts = [1, 4096], - args = PRODUCER_COUNTS, - sample_count = 50, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] -fn sustained_capacity( - bencher: Bencher, - producer_count: usize, -) { - let mut batch = RepeatedBatch::>::new(producer_count); - batch.run(); - bencher.bench_local(|| batch.run()); -} - -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - consts = [1, 64, 4096], + consts = [1, 64], args = [(1, 0), (8, 0), (1, 4), (8, 4)], sample_count = 50, sample_size = 1, @@ -126,8 +95,8 @@ fn scheduled( #[divan::bench( types = [Asyncband, Tokio, AsyncChannel, Flume], - consts = [1, 64, 4096], - args = [(1, 0), (8, 0), (1, 4), (8, 4)], + consts = [1, 64], + args = [(1, 4), (8, 4)], sample_count = 50, sample_size = 1, counter = ItemsCount::new(BATCH_MESSAGES), @@ -143,32 +112,27 @@ fn scheduled_inline, const CAPACITY: usize>( #[divan::bench( types = [Asyncband, Tokio, AsyncChannel, Flume], - consts = [1, 64, 4096], args = [1, 8], sample_count = 50, sample_size = 1, counter = ItemsCount::new(BATCH_MESSAGES), )] -fn external_receiver(bencher: Bencher, producers: usize) { - let mut batch = RepeatedTasks::>::external_receiver(producers, 4); +fn external_receiver(bencher: Bencher, producers: usize) { + let mut batch = RepeatedTasks::>::external_receiver(producers, 4); batch.run(); bencher.bench_local(|| batch.run()); } #[divan::bench( types = [Asyncband, Tokio, AsyncChannel, Flume], - consts = [1, 64, 4096], args = [1, 8], sample_count = 50, sample_size = 1, counter = ItemsCount::new(BATCH_MESSAGES), )] -fn external_receiver_inline, const CAPACITY: usize>( - bencher: Bencher, - producers: usize, -) { +fn external_receiver_inline>(bencher: Bencher, producers: usize) { let mut batch = - RepeatedTasks::>::external_receiver(producers, 4); + RepeatedTasks::>::external_receiver(producers, 4); batch.run(); bencher.bench_local(|| batch.run()); } diff --git a/benchmarks/ecosystem/mpsc/support.rs b/benchmarks/ecosystem/mpsc/support.rs index 1f1957e..d1d64c4 100644 --- a/benchmarks/ecosystem/mpsc/support.rs +++ b/benchmarks/ecosystem/mpsc/support.rs @@ -31,7 +31,7 @@ use super::adapters::UnboundedMpsc; pub const BOUNDED_CAPACITY: usize = 64; pub const BATCH_MESSAGES: usize = 16_384; -pub const PRODUCER_COUNTS: &[usize] = &[1, 2, 4, 8]; +pub const PRODUCER_COUNTS: &[usize] = &[1, 8]; pub trait Message: Send + 'static { fn new(sequence: usize) -> Self; @@ -134,63 +134,6 @@ impl ConcurrentMpsc for Unbounded { } } -pub struct ConcurrentBatch> { - receiver: C::Receiver, - start: Arc, - workers: Vec>, -} - -impl> ConcurrentBatch { - pub fn new(producer_count: usize) -> Self { - assert_eq!(BATCH_MESSAGES % producer_count, 0); - - let (sender, receiver) = C::channel(); - let start = Arc::new(Barrier::new(producer_count + 1)); - let messages_per_producer = BATCH_MESSAGES / producer_count; - let workers = (0..producer_count) - .map(|producer| { - let sender = sender.clone(); - let start = start.clone(); - thread::spawn(move || { - start.wait(); - let first = producer * messages_per_producer; - for offset in 0..messages_per_producer { - C::send(&sender, black_box(first + offset)); - } - }) - }) - .collect(); - drop(sender); - - Self { - receiver, - start, - workers, - } - } - - pub fn run(&mut self) -> usize { - self.start.wait(); - let mut checksum = 0usize; - for _ in 0..BATCH_MESSAGES { - checksum = checksum.wrapping_add(C::recv(&mut self.receiver)); - } - black_box(checksum) - } -} - -impl> Drop for ConcurrentBatch { - fn drop(&mut self) { - let panicking = thread::panicking(); - for worker in self.workers.drain(..) { - let result = worker.join(); - if !panicking { - result.expect("benchmark producer panicked"); - } - } - } -} - // Reuse worker threads and channel storage so steady-state samples exclude thread creation. pub struct RepeatedBatch> { receiver: C::Receiver, diff --git a/benchmarks/ecosystem/mpsc/unbounded.rs b/benchmarks/ecosystem/mpsc/unbounded.rs index b2e79d0..a01531e 100644 --- a/benchmarks/ecosystem/mpsc/unbounded.rs +++ b/benchmarks/ecosystem/mpsc/unbounded.rs @@ -32,7 +32,6 @@ use super::adapters::Flume; use super::adapters::Tokio; use super::adapters::UnboundedMpsc; use super::support::BATCH_MESSAGES; -use super::support::ConcurrentBatch; use super::support::PRODUCER_COUNTS; use super::support::RepeatedBatch; use super::support::RepeatedTasks; @@ -60,19 +59,6 @@ fn try_round_trip(bencher: Bencher) { }); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = PRODUCER_COUNTS, - sample_count = 20, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] -fn concurrent(bencher: Bencher, producer_count: usize) { - bencher - .with_inputs(|| ConcurrentBatch::>::new(producer_count)) - .bench_local_refs(|batch| batch.run()); -} - #[divan::bench( types = [Asyncband, Tokio, AsyncChannel, Flume], args = [32, 1024, 65_536], @@ -86,7 +72,7 @@ fn burst_drain(bencher: Bencher, messages: usize) { #[divan::bench( types = [Asyncband, Tokio, AsyncChannel, Flume], consts = [64, 1024], - args = [32, 1024, 65_536], + args = [1024], sample_count = 20, sample_size = 1, )] @@ -99,7 +85,7 @@ fn burst_drain_inline, const SIZE: usize>( #[divan::bench( types = [Asyncband, Tokio, AsyncChannel, Flume], - args = [32, 1024, 65_536], + args = [1024], sample_count = 20, sample_size = 1, )] @@ -164,7 +150,7 @@ fn clone_drop_sender(bencher: Bencher) { #[divan::bench( types = [Asyncband, Tokio, AsyncChannel, Flume], - args = [(1, 0), (4, 0), (1, 4), (4, 4), (8, 4)], + args = [(1, 0), (8, 0), (1, 4), (8, 4)], sample_count = 50, sample_size = 1, counter = ItemsCount::new(BATCH_MESSAGES), @@ -177,7 +163,7 @@ fn scheduled(bencher: Bencher, (producers, workers): (usize, u #[divan::bench( types = [Asyncband, Tokio, AsyncChannel, Flume], - args = [(1, 64), (4, 64), (8, 64), (1, 1024), (4, 1024), (8, 1024)], + args = [(1, 64), (8, 64), (1, 1024), (8, 1024)], sample_count = 50, sample_size = 1, counter = ItemsCount::new(BATCH_MESSAGES), From 21af06e8f1865d02c20d89af477ad046716479ab Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:41:52 +0800 Subject: [PATCH 17/26] test: establish pending waiters before releasing conditions Signed-off-by: tison --- .../tests/mpsc_test/concurrency.rs | 12 +++-- tests-integration/tests/once_map_test.rs | 48 ++++++++----------- tests-integration/tests/semaphore_test.rs | 37 +++++++------- 3 files changed, 46 insertions(+), 51 deletions(-) diff --git a/tests-integration/tests/mpsc_test/concurrency.rs b/tests-integration/tests/mpsc_test/concurrency.rs index d5c50a8..9df1929 100644 --- a/tests-integration/tests/mpsc_test/concurrency.rs +++ b/tests-integration/tests/mpsc_test/concurrency.rs @@ -296,15 +296,21 @@ fn unbounded_collects_from_multiple_producers() { #[cfg_attr(miri, ignore = "requires an OS-backed Tokio runtime")] async fn bounded_backpressure_progresses_on_an_executor() { let (tx, mut rx) = mpsc::bounded(1); + let (blocked_tx, blocked_rx) = tokio::sync::oneshot::channel(); tx.send(1).await.unwrap(); - // This will block until the receiver is ready to receive. - tokio::spawn(async move { - tx.send(2).await.unwrap(); + let sender = tokio::spawn(async move { + let mut send = std::pin::pin!(tx.send(2)); + let first_poll = std::future::poll_fn(|cx| Poll::Ready(send.as_mut().poll(cx))).await; + assert!(first_poll.is_pending()); + blocked_tx.send(()).unwrap(); + send.await.unwrap(); }); + blocked_rx.await.unwrap(); assert_eq!(Ok(1), rx.recv().await); assert_eq!(Ok(2), rx.recv().await); + sender.await.unwrap(); assert_eq!(Err(RecvError::Disconnected), rx.recv().await); } diff --git a/tests-integration/tests/once_map_test.rs b/tests-integration/tests/once_map_test.rs index 50cc61f..df5fea6 100644 --- a/tests-integration/tests/once_map_test.rs +++ b/tests-integration/tests/once_map_test.rs @@ -25,6 +25,7 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use asyncband::once::OnceMap; +use tests_integration::poll_once; #[test] fn constructors_and_default() { @@ -43,42 +44,35 @@ async fn compute_caches_value() { #[tokio::test] async fn concurrent_compute_runs_once() { - let map = Arc::new(OnceMap::new()); - let count = Arc::new(AtomicUsize::new(0)); - let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let map = OnceMap::new(); + let count = AtomicUsize::new(0); let (release_tx, release_rx) = tokio::sync::oneshot::channel(); - let map_clone = map.clone(); - let count_clone = count.clone(); - let first = tokio::spawn(async move { - map_clone - .compute("key", async move || { - count_clone.fetch_add(1, Ordering::SeqCst); - started_tx.send(()).unwrap(); - release_rx.await.unwrap(); - 42 - }) - .await + let first = map.compute("key", async || { + count.fetch_add(1, Ordering::SeqCst); + release_rx.await.unwrap(); + 42 }); + tokio::pin!(first); + assert!(poll_once(first.as_mut()).is_pending()); - started_rx.await.unwrap(); - let mut waiters = vec![]; - for _ in 0..9 { - let map = map.clone(); - let count = count.clone(); - waiters.push(tokio::spawn(async move { - map.compute("key", async move || { + let mut waiters = (0..9) + .map(|_| { + Box::pin(map.compute("key", async || { count.fetch_add(1, Ordering::SeqCst); - 42 - }) - .await - })); + 99 + })) + }) + .collect::>(); + for waiter in &mut waiters { + assert!(poll_once(waiter.as_mut()).is_pending()); } + assert_eq!(count.load(Ordering::SeqCst), 1); release_tx.send(()).unwrap(); - assert_eq!(first.await.unwrap(), 42); + assert_eq!(first.await, 42); for waiter in waiters { - assert_eq!(waiter.await.unwrap(), 42); + assert_eq!(waiter.await, 42); } assert_eq!(count.load(Ordering::SeqCst), 1); } diff --git a/tests-integration/tests/semaphore_test.rs b/tests-integration/tests/semaphore_test.rs index 4c46bd5..7615f58 100644 --- a/tests-integration/tests/semaphore_test.rs +++ b/tests-integration/tests/semaphore_test.rs @@ -25,6 +25,8 @@ use std::task::Waker; use asyncband::semaphore::Semaphore; use tests_integration::PanicWake; use tests_integration::WakeCounter; +use tests_integration::expect_ready; +use tests_integration::poll_with; #[test] fn no_permits() { @@ -45,27 +47,20 @@ fn try_acquire() { assert!(p3.is_some()); } -#[tokio::test] -async fn acquire() { - let sem = Arc::new(Semaphore::new(1)); - let p1 = sem.try_acquire(1).unwrap(); - let sem_clone = sem.clone(); - let j = tokio::spawn(async move { - let _p2 = sem_clone.acquire(1).await; - }); - drop(p1); - j.await.unwrap(); -} - -#[tokio::test] -async fn add_permits() { - let sem = Arc::new(Semaphore::new(0)); - let sem_clone = sem.clone(); - let j = tokio::spawn(async move { - let _p2 = sem_clone.acquire(1).await; - }); - sem.release(1); - j.await.unwrap(); +#[test] +fn released_permit_wakes_a_pending_acquire() { + let sem = Semaphore::new(1); + let held = sem.try_acquire(1).unwrap(); + let mut acquire = pin!(sem.acquire(1)); + let (waker, wakes) = WakeCounter::new(); + assert!(poll_with(acquire.as_mut(), &waker).is_pending()); + + drop(held); + assert_eq!(wakes.count(), 1); + let permit = expect_ready(poll_with(acquire.as_mut(), &waker)); + assert_eq!(sem.available_permits(), 0); + drop(permit); + assert_eq!(sem.available_permits(), 1); } #[test] From 3bebe7b98ff94c5299b46b2901d6958d3f4cd77b Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:42:01 +0800 Subject: [PATCH 18/26] test(mpsc): verify every producer message exactly once Signed-off-by: tison --- tests-integration/tests/mpsc_test/concurrency.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests-integration/tests/mpsc_test/concurrency.rs b/tests-integration/tests/mpsc_test/concurrency.rs index 9df1929..8937ec4 100644 --- a/tests-integration/tests/mpsc_test/concurrency.rs +++ b/tests-integration/tests/mpsc_test/concurrency.rs @@ -284,11 +284,12 @@ fn unbounded_collects_from_multiple_producers() { } drop(tx); - let mut sum = 0; + let mut received = Vec::new(); while let Ok(i) = rx.recv().await { - sum += i; + received.push(i); } - assert_eq!(sum, 28); + received.sort_unstable(); + assert_eq!(received, (0..8).collect::>()); }); } From 69b4e2b2dc220e4f6ac8e5a28278492bed7f288b Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:43:32 +0800 Subject: [PATCH 19/26] test(once_map): retain public lifecycle and collision coverage Signed-off-by: tison --- asyncband/src/once/once_map/tests.rs | 26 ------------------------ tests-integration/tests/once_map_test.rs | 26 +----------------------- 2 files changed, 1 insertion(+), 51 deletions(-) diff --git a/asyncband/src/once/once_map/tests.rs b/asyncband/src/once/once_map/tests.rs index b9b8ae1..fe25d1c 100644 --- a/asyncband/src/once/once_map/tests.rs +++ b/asyncband/src/once/once_map/tests.rs @@ -132,32 +132,6 @@ fn failed_compute_preserves_entry_for_waiter_retry() { assert_eq!(map.get("key"), Some(1)); } -#[test] -fn abandoned_pending_entry_is_removed_when_last_caller_leaves() { - let map = OnceMap::<&str, i32>::new(); - let Lookup::Pending(entry) = map.get_or_insert("key") else { - unreachable!() - }; - - map.cleanup_abandoned_entry(entry); - - assert_eq!(map.len(), 0); -} - -#[test] -fn colliding_ready_entries_can_be_unlinked_independently() { - let map: OnceMap> = - (0..4).map(|key| (key, key * 2)).collect(); - - map.discard(&1); - map.discard(&3); - - assert_eq!(map.get(&0), Some(0)); - assert_eq!(map.get(&1), None); - assert_eq!(map.get(&2), Some(4)); - assert_eq!(map.get(&3), None); -} - #[test] fn colliding_pending_entries_are_tracked_independently() { let map: OnceMap> = OnceMap::default(); diff --git a/tests-integration/tests/once_map_test.rs b/tests-integration/tests/once_map_test.rs index df5fea6..6b2c61d 100644 --- a/tests-integration/tests/once_map_test.rs +++ b/tests-integration/tests/once_map_test.rs @@ -221,6 +221,7 @@ async fn remove_while_computing_allows_a_new_generation() { }); started_rx.await.unwrap(); + assert_eq!(map.get("key"), None); assert_eq!(map.remove("key"), None); assert_eq!(map.compute("key", async || 2).await, 2); release_tx.send(()).unwrap(); @@ -229,31 +230,6 @@ async fn remove_while_computing_allows_a_new_generation() { assert_eq!(map.get("key"), Some(2)); } -#[tokio::test] -async fn get_returns_none_while_computing() { - let map = Arc::new(OnceMap::new()); - let (started_tx, started_rx) = tokio::sync::oneshot::channel(); - let (release_tx, release_rx) = tokio::sync::oneshot::channel(); - - let map_clone = map.clone(); - let task = tokio::spawn(async move { - map_clone - .compute("key", async move || { - started_tx.send(()).unwrap(); - release_rx.await.unwrap(); - 1 - }) - .await - }); - - started_rx.await.unwrap(); - assert_eq!(map.get("key"), None); - release_tx.send(()).unwrap(); - - assert_eq!(task.await.unwrap(), 1); - assert_eq!(map.get("key"), Some(1)); -} - #[test] fn from_iter_keeps_last_value_for_duplicate_key() { #[derive(Hash, PartialEq, Eq)] From bc838446da9c14e82baf83f896aeaf0b122a814b Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:43:35 +0800 Subject: [PATCH 20/26] test(wakerset): rely on public deferred-drop coverage Signed-off-by: tison --- asyncband/src/internal/wakerset.rs | 52 +----------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/asyncband/src/internal/wakerset.rs b/asyncband/src/internal/wakerset.rs index eb11366..c49d49f 100644 --- a/asyncband/src/internal/wakerset.rs +++ b/asyncband/src/internal/wakerset.rs @@ -111,65 +111,15 @@ impl WakerSet { pub fn unregister(&mut self, token: &mut Option) -> Option { token.take().map(|token| self.wakers.remove(token.0)) } - - #[cfg(test)] - fn registered_len(&self) -> usize { - self.wakers.len() - } } #[cfg(test)] mod tests { - use std::mem::size_of; - use std::sync::Arc; - use std::sync::atomic::AtomicBool; - use std::sync::atomic::AtomicUsize; - use std::sync::atomic::Ordering; - use std::task::Wake; - - use super::*; - - struct DropWake { - dropped: Arc, - wake_count: AtomicUsize, - } - - impl Wake for DropWake { - fn wake(self: Arc) { - self.wake_count.fetch_add(1, Ordering::Relaxed); - } - } - - impl Drop for DropWake { - fn drop(&mut self) { - self.dropped.store(true, Ordering::Relaxed); - } - } + use super::WakerToken; #[test] fn waker_token_preserves_the_option_niche() { assert_eq!(size_of::(), size_of::()); assert_eq!(size_of::(), size_of::>()); } - - #[test] - fn unregister_returns_the_waker_for_deferred_drop() { - let dropped = Arc::new(AtomicBool::new(false)); - let waker = Waker::from(Arc::new(DropWake { - dropped: dropped.clone(), - wake_count: AtomicUsize::new(0), - })); - let mut wakers = WakerSet::new(); - let mut token = None; - - drop(wakers.register(&mut token, &waker)); - drop(waker); - - let removed = wakers.unregister(&mut token); - assert_eq!(wakers.registered_len(), 0); - assert!(!dropped.load(Ordering::Relaxed)); - - drop(removed); - assert!(dropped.load(Ordering::Relaxed)); - } } From f661e6f9dc202b6f32dbd263975144c5490f63ad Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:43:41 +0800 Subject: [PATCH 21/26] test(broadcast): remove subsumed delivery cases Signed-off-by: tison --- .../tests/broadcast_mpmc_unbounded_test.rs | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/tests-integration/tests/broadcast_mpmc_unbounded_test.rs b/tests-integration/tests/broadcast_mpmc_unbounded_test.rs index 9012051..ad20069 100644 --- a/tests-integration/tests/broadcast_mpmc_unbounded_test.rs +++ b/tests-integration/tests/broadcast_mpmc_unbounded_test.rs @@ -83,48 +83,6 @@ impl Rng { } } -#[tokio::test] -async fn test_broadcast_basic() { - let (tx, mut rx1) = unbounded(); - let mut rx2 = tx.subscribe(); - - tx.send(10); - tx.send(20); - - assert_eq!(rx1.recv().await, Ok(10)); - assert_eq!(rx1.recv().await, Ok(20)); - assert_eq!(rx2.recv().await, Ok(10)); - assert_eq!(rx2.recv().await, Ok(20)); -} - -#[tokio::test] -async fn test_subscribe() { - let (tx, _rx) = unbounded(); - let mut rx = tx.subscribe(); - - tx.send(100); - assert_eq!(rx.recv().await, Ok(100)); -} - -#[tokio::test] -async fn test_resubscribe() { - let (tx, mut rx) = unbounded(); - - tx.send(1); - tx.send(2); - - let mut rx2 = rx.resubscribe(); - - // rx sees 1, 2 - // rx2 sees nothing yet (starts at tail=2) - - tx.send(3); - - assert_eq!(rx.recv().await, Ok(1)); - assert_eq!(rx.recv().await, Ok(2)); - assert_eq!(rx2.recv().await, Ok(3)); -} - #[test] fn test_try_recv() { let (tx, mut rx) = unbounded(); From 85b391ae4b3780dab14007c78614a54d4ce30527 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:43:45 +0800 Subject: [PATCH 22/26] test(mutex): consolidate mapped guard ownership coverage Signed-off-by: tison --- tests-integration/tests/mutex_test.rs | 37 --------------------------- 1 file changed, 37 deletions(-) diff --git a/tests-integration/tests/mutex_test.rs b/tests-integration/tests/mutex_test.rs index b9e426d..8dd1107 100644 --- a/tests-integration/tests/mutex_test.rs +++ b/tests-integration/tests/mutex_test.rs @@ -47,24 +47,6 @@ fn test_get_mut_provides_exclusive_access() { assert_eq!(inner, 100); } -#[tokio::test] -async fn test_guard_map_preserves_lock() { - let data = (99i32, vec![1, 2, 3]); - let mutex = Mutex::new(data); - - let guard = mutex.lock().await; - let mut mapped_guard = MutexGuard::map(guard, |data| &mut data.0); - - assert!(mutex.try_lock().is_none()); - - *mapped_guard = 100; - - // After dropping, mutex should be available - drop(mapped_guard); - let guard = mutex.try_lock().unwrap(); - assert_eq!(guard.0, 100); -} - #[tokio::test] async fn test_owned_mapped_guard_holds_lock() { // Test that mapped owned guard properly holds the lock @@ -123,25 +105,6 @@ async fn test_owned_guard_filter_map_failure() { } } -#[tokio::test] -async fn test_multiple_map_operations() { - // Test multiple consecutive map operations - let data = vec![vec![1, 2], vec![3, 4]]; - let mutex = Mutex::new(data); - - let guard = mutex.lock().await; - let first_vec = MutexGuard::map(guard, |data| &mut data[0]); - let mut first_element = MappedMutexGuard::map(first_vec, |vec| &mut vec[0]); - - *first_element = 100; - drop(first_element); - - let guard = mutex.lock().await; - assert_eq!(guard[0][0], 100); - assert_eq!(guard[0][1], 2); - assert_eq!(guard[1][0], 3); -} - #[test] fn test_guard_prevents_concurrent_access() { let mutex = Mutex::new(0); From f79c080cfb817caab97f3a4395b53a6f33fac886 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:43:49 +0800 Subject: [PATCH 23/26] test(semaphore): remove subsumed permit cases Signed-off-by: tison --- tests-integration/tests/semaphore_test.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests-integration/tests/semaphore_test.rs b/tests-integration/tests/semaphore_test.rs index 7615f58..a3f4f67 100644 --- a/tests-integration/tests/semaphore_test.rs +++ b/tests-integration/tests/semaphore_test.rs @@ -83,12 +83,6 @@ fn add_max_amount_permits() { assert_eq!(s.available_permits(), usize::MAX); } -#[test] -#[should_panic(expected = "would overflow usize::MAX")] -fn release_overflow_panics() { - Semaphore::new(usize::MAX).release(1); -} - #[test] fn release_overflow_preserves_permits() { let s = Semaphore::new(usize::MAX); @@ -144,18 +138,6 @@ fn no_panic_at_max_permits() { s.release(1); } -#[test] -fn try_acquire_concurrently() { - let s = Semaphore::new(1); - let p1 = s.try_acquire(1).unwrap(); - assert_eq!(s.available_permits(), 0); - let p2 = s.try_acquire(1); - assert!(p2.is_none()); - assert_eq!(s.available_permits(), 0); - drop(p1); - assert_eq!(s.available_permits(), 1); -} - #[test] fn acquire_then_drop() { let waker = Waker::noop(); From 7a429bc4b7f84e531e4c06fd25b83bdeb1bdf5ee Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:43:53 +0800 Subject: [PATCH 24/26] test(singleflight): keep the stronger basic computation case Signed-off-by: tison --- tests-integration/tests/singleflight_test.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests-integration/tests/singleflight_test.rs b/tests-integration/tests/singleflight_test.rs index 7d19929..87008f3 100644 --- a/tests-integration/tests/singleflight_test.rs +++ b/tests-integration/tests/singleflight_test.rs @@ -21,13 +21,6 @@ use std::sync::atomic::Ordering; use asyncband::singleflight::Group; use tests_integration::poll_once; -#[tokio::test] -async fn work_returns_value() { - let group = Group::new(); - let res = group.work("key", || async { "val" }).await; - assert_eq!(res, "val"); -} - #[tokio::test] async fn supports_non_clone_key() { #[derive(Hash, PartialEq, Eq)] From c5b907c3c406a08ceddc3b7e934060991d3c541f Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:43:57 +0800 Subject: [PATCH 25/26] test(semaphore): keep node reclamation checks next to state Signed-off-by: tison --- asyncband/src/internal/semaphore.rs | 22 +++++++++++++------ asyncband/src/semaphore/mod.rs | 3 --- asyncband/src/semaphore/tests.rs | 33 ----------------------------- 3 files changed, 15 insertions(+), 43 deletions(-) delete mode 100644 asyncband/src/semaphore/tests.rs diff --git a/asyncband/src/internal/semaphore.rs b/asyncband/src/internal/semaphore.rs index 3828361..8f9890e 100644 --- a/asyncband/src/internal/semaphore.rs +++ b/asyncband/src/internal/semaphore.rs @@ -256,11 +256,6 @@ impl Semaphore { wakers.wake_all(); } } - - #[cfg(test)] - pub fn num_waiter_nodes(&self) -> usize { - self.waiters.lock().occupied_len() - } } #[derive(Debug)] @@ -448,6 +443,19 @@ mod tests { } } + #[test] + fn fulfilled_reduce_permits_debt_reclaims_its_waiter_node() { + let semaphore = Semaphore::new(0); + + for _ in 0..3 { + semaphore.reduce_permits(1); + assert_eq!(semaphore.waiters.lock().occupied_len(), 1); + + semaphore.release(1); + assert_eq!(semaphore.waiters.lock().occupied_len(), 0); + } + } + #[test] fn release_drains_more_than_one_wake_batch() { const WAITER_COUNT: usize = WAKE_BATCH_SIZE + 3; @@ -462,7 +470,7 @@ mod tests { for acquire in &mut acquires { assert!(acquire.poll_once(&waker).is_pending()); } - assert_eq!(semaphore.num_waiter_nodes(), WAITER_COUNT); + assert_eq!(semaphore.waiters.lock().occupied_len(), WAITER_COUNT); semaphore.release(WAITER_COUNT); assert_eq!(counter.0.load(Ordering::Relaxed), WAITER_COUNT); @@ -470,6 +478,6 @@ mod tests { for acquire in &mut acquires { assert!(acquire.poll_once(&waker).is_ready()); } - assert_eq!(semaphore.num_waiter_nodes(), 0); + assert_eq!(semaphore.waiters.lock().occupied_len(), 0); } } diff --git a/asyncband/src/semaphore/mod.rs b/asyncband/src/semaphore/mod.rs index 2910fa7..67d0a17 100644 --- a/asyncband/src/semaphore/mod.rs +++ b/asyncband/src/semaphore/mod.rs @@ -55,9 +55,6 @@ use std::sync::Arc; use crate::internal::semaphore; -#[cfg(test)] -mod tests; - /// An async counting semaphore for controlling access to a set of resources. /// /// See the [module level documentation](self) for more. diff --git a/asyncband/src/semaphore/tests.rs b/asyncband/src/semaphore/tests.rs deleted file mode 100644 index d408dcb..0000000 --- a/asyncband/src/semaphore/tests.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use super::Semaphore; - -// This test stays next to the implementation because it inspects private state. - -#[test] -fn fulfilled_reduce_permits_debt_reclaims_its_waiter_node() { - let semaphore = Semaphore::new(0); - - for _ in 0..3 { - semaphore.reduce_permits(1); - assert_eq!(semaphore.s.num_waiter_nodes(), 1); - - semaphore.release(1); - assert_eq!(semaphore.s.num_waiter_nodes(), 0); - } -} From f5fc4799a746f85ea89ca52812a4ce7b2055c9d3 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 9 Sep 2026 20:44:02 +0800 Subject: [PATCH 26/26] docs(xtask): correct no-capture execution help Signed-off-by: tison --- xtask/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 9074ed0..41c5a87 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -126,7 +126,7 @@ impl CommandMiri { #[derive(Parser)] struct CommandTest { - #[arg(long, help = "Run tests serially and do not capture output.")] + #[arg(long, help = "Do not capture test output.")] no_capture: bool, }