blob: e99582ceed3272b00da9d6ec84d1c3e04daa67b7 [file]
// Copyright 2026 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
use crate::kernel::event::Event;
use core::pin::Pin;
use core::ptr::NonNull;
use core::sync::atomic::{AtomicU64, Ordering};
use fbl::{
DoublyLinkedList, DoublyLinkedListContainable, DoublyLinkedListNode, Recyclable, RefPtr,
ref_counted,
};
use ksync::{KMutex, KMutexGuard, LockToken, PhantomMutex, RawMutex, guarded};
use pin_init::{PinInit, pin_data, pin_init, pinned_drop};
use zx_status::Status;
use zx_types::{ZX_OK, zx_status_t};
/// Tag for the write queue in `StreamSizeManager`.
struct WriteQueueTag;
/// Tag for the read queue in `StreamSizeManager`.
struct ReadQueueTag;
/// The type of operation being managed by `StreamSizeManager`.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperationType {
Write = 0,
Read = 1,
SetSize = 2,
Append = 3,
}
/// `Operation` is a structure to ensure operations related to stream size are committed in order.
/// `Operation` is intended to be used as a stack-allocated structure.
///
/// Currently, an operation maps 1:1 with the thread it is executing on and thus, can be
/// considered owned by that thread.
///
/// Notes:
/// * The initialization, destruction, and immutable properties of this type are only
/// thread-compatible.
/// * The type must either be committed or cancelled before destruction. Otherwise, the destructor
/// will panic.
#[guarded]
#[repr(C)]
#[derive(DoublyLinkedListContainable)]
#[pin_data(PinnedDrop)]
pub struct Operation {
#[dll_node(tag = WriteQueueTag)]
write_node: DoublyLinkedListNode<Operation>,
#[dll_node(tag = ReadQueueTag)]
read_node: DoublyLinkedListNode<Operation>,
#[pin]
ready_event: Event,
#[mutex(StreamSizeManagerLockClass)]
phantom_lock: KMutex<PhantomMutex>,
parent: *const StreamSizeManager,
#[guarded_by(phantom_lock)]
valid: bool,
#[guarded_by(phantom_lock)]
op_type: OperationType,
/// Holds the target size. For appends, this will only be valid once the operation is at the
/// head of the queue.
#[guarded_by(phantom_lock)]
size: u64,
/// Tracks any stream size updated this operation performed. This is used to ensure that an
/// operation is not cancelled or shrunk beyond any published stream size updates.
#[cfg(debug_assertions)]
committed_stream_size: AtomicU64,
}
impl Operation {
/// Constructs a `PinInit` initializer for `Operation`.
pub fn init<'a>(
parent: &'a StreamSizeManager,
) -> impl PinInit<Self, core::convert::Infallible> + 'a {
pin_init!(Self {
write_node: DoublyLinkedListNode::new(),
read_node: DoublyLinkedListNode::new(),
ready_event <- Event::init_unsignaled(),
phantom_lock: KMutex::new(PhantomMutex),
parent: parent as *const StreamSizeManager,
valid: false.into(),
op_type: OperationType::Read.into(),
size: 0.into(),
#[cfg(debug_assertions)]
committed_stream_size: AtomicU64::new(0),
})
}
/// Returns a reference to the parent stream size manager's mutex lock.
#[inline]
pub fn lock(&self) -> &KMutex<StreamSizeManagerLockClass, RawMutex> {
// SAFETY: `self.parent` points to a live, pinned `StreamSizeManager` that outlives this
// operation because the operation holds a reference/borrow to its parent.
unsafe { (*self.parent).lock() }
}
/// Returns a reference to the parent `StreamSizeManager` while holding the manager's lock.
#[inline]
fn parent_locked<'a>(
&self,
_token: &LockToken<'a, StreamSizeManagerLockClass>,
) -> &'a StreamSizeManager {
// SAFETY: `self.parent` points to a live `StreamSizeManager` initialized during
// `Operation::init`. The caller holds the manager's lock as proven by the lock token with
// lifetime `'a`.
unsafe { &*self.parent }
}
/// Indicates whether the operation is valid.
#[inline]
pub fn is_valid_locked(&self, token: &LockToken<'_, StreamSizeManagerLockClass>) -> bool {
*self.guard_phantom_lock(token).valid()
}
/// Resets validity of the operation. This operation is private as it is assumed to only be
/// called when it is correct to do so by the StreamSizeManager.
#[inline]
fn reset(&self, token: &mut LockToken<'_, StreamSizeManagerLockClass>) {
*self.guard_phantom_lock_mut(token).valid_mut() = false;
}
fn initialize(
&self,
parent: *const StreamSizeManager,
size: u64,
op_type: OperationType,
token: &mut LockToken<'_, StreamSizeManagerLockClass>,
) {
debug_assert!(!*self.guard_phantom_lock(token).valid());
debug_assert!(core::ptr::eq(self.parent, parent));
let mut guard = self.guard_phantom_lock_mut(token);
*guard.valid_mut() = true;
*guard.size_mut() = size;
*guard.op_type_mut() = op_type;
}
/// Gets the operation's type while holding the parent lock.
#[inline]
pub fn get_type_locked(
&self,
token: &LockToken<'_, StreamSizeManagerLockClass>,
) -> OperationType {
*self.guard_phantom_lock(token).op_type()
}
/// Gets the stream size that the operation will expand to once it is completed.
///
/// Note:
/// * This may only be called on a valid operation.
/// * This must only be called when holding the parent `StreamSizeManager` lock.
pub fn get_size_locked(&self, token: &LockToken<'_, StreamSizeManagerLockClass>) -> u64 {
let guard = self.guard_phantom_lock(token);
debug_assert!(*guard.valid());
// Reading the size for `Append` operations should only occur after it's been initialized.
debug_assert!(*guard.op_type() != OperationType::Append || *guard.size() > 0);
*guard.size()
}
/// Shrinks the size of the operation.
///
/// Only size shrinks are allowed, since the concurrency of other operations are gated on the
/// largest potential size of operations in front of it.
///
/// Note:
/// * This may only be called on a valid operation.
/// * This must only be called when holding the parent `StreamSizeManager` lock.
/// * The `new_size` passed in must be greater than 0.
/// * The `new_size` passed in must be less than or equal to the current size.
/// * This must only be called for `OperationType::Append` and `OperationType::Write` ops.
pub fn shrink_size_locked(
&self,
token: &mut LockToken<'_, StreamSizeManagerLockClass>,
new_size: u64,
) {
let mut guard = self.guard_phantom_lock_mut(token);
debug_assert!(*guard.valid());
// If `new_size` is 0, the operation should be cancelled instead.
debug_assert!(new_size > 0);
// This function may only be called on expanding stream write operations.
assert!(
*guard.op_type() == OperationType::Append || *guard.op_type() == OperationType::Write
);
assert!(new_size <= *guard.size());
#[cfg(debug_assertions)]
debug_assert!(new_size >= self.committed_stream_size.load(Ordering::Relaxed));
*guard.size_mut() = new_size;
}
/// Commits the operation's effects on the stream size.
///
/// Note:
/// * This may only be called on a valid operation.
/// * This must only be called when holding the parent `StreamSizeManager` lock.
pub fn commit_locked(&self, token: &mut LockToken<'_, StreamSizeManagerLockClass>) {
debug_assert!(self.is_valid_locked(token));
self.parent_locked(token).commit_and_dequeue_operation_locked(self, token);
}
/// Cancels the operation and does not commit any changes to the stream size.
///
/// Note:
/// * This may only be called on a valid operation.
/// * This must only be called when holding the parent `StreamSizeManager` lock.
pub fn cancel_locked(&self, token: &mut LockToken<'_, StreamSizeManagerLockClass>) {
debug_assert!(self.is_valid_locked(token));
#[cfg(debug_assertions)]
debug_assert!(self.committed_stream_size.load(Ordering::Relaxed) == 0);
self.parent_locked(token).dequeue_operation_locked(self, token);
}
#[cfg(debug_assertions)]
fn debug_assert_valid_progress(&self, new_stream_size: u64) {
// In the original C++ implementation, `Operation`'s fields (`valid_`, `type_`, `size_`)
// were plain member variables without `TA_GUARDED` annotations, while thread-safety
// annotations (`TA_REQ(lock())`) were placed only on mutating or querying methods.
// `UpdateStreamSizeFromProgress` was intentionally unannotated because it executes on the
// thread performing the streaming write without holding the `StreamSizeManager` lock.
// Because `Operation` is stack-allocated on and owned 1:1 by the executing thread, that
// thread could safely inspect its own `valid_`, `type_`, and `size_` without a lock.
//
// In Rust, these fields are wrapped in `KCell` guarded by `phantom_lock` so that other
// threads traversing queues in `StreamSizeManager` can only inspect them while holding the
// lock. On the operation's owning thread, no concurrent writes to `valid`, `op_type`, or
// `size` can occur during streaming I/O. Therefore, we can construct a temporary
// `LockToken` using `unsafe { LockToken::new() }` solely to inspect these fields for debug
// assertions.
//
// SAFETY: `update_stream_size_from_progress` is only called by the thread that owns this
// stack-allocated `Operation`. No other thread modifies `valid`, `op_type`, or `size`, and
// the constructed `LockToken` does not escape this function scope.
let token = unsafe { LockToken::<StreamSizeManagerLockClass>::new() };
let guard = self.guard_phantom_lock(&token);
debug_assert!(*guard.valid());
let op_type = *guard.op_type();
debug_assert!(op_type == OperationType::Write || op_type == OperationType::Append);
debug_assert!(new_stream_size <= *guard.size());
}
/// Updates the stream size when progress is made from the operation. Once this has been called
/// it is invalid to call `cancel_locked` or to call `shrink_size_locked` with a size less than
/// the stream size provided here.
///
/// Note:
/// * This may only be called on a valid `Append` or `Write` operation.
/// * The stream size must be larger than the current stream size.
pub fn update_stream_size_from_progress(&self, new_stream_size: u64) {
// SAFETY: `self.parent` points to a live `StreamSizeManager` instance. Atomic updates
// to `stream_size` handle internal thread synchronization.
let parent = unsafe { &*self.parent };
#[cfg(debug_assertions)]
self.debug_assert_valid_progress(new_stream_size);
debug_assert!(new_stream_size > parent.get_stream_size());
#[cfg(debug_assertions)]
self.committed_stream_size.store(new_stream_size, Ordering::Relaxed);
parent.set_stream_size(new_stream_size);
}
}
#[pinned_drop]
impl PinnedDrop for Operation {
fn drop(self: Pin<&mut Self>) {
let me = unsafe { self.get_unchecked_mut() };
debug_assert!(
!*me.valid.get_inner_mut(),
"Operation destructed without cancelling or committing!"
);
}
}
/// `StreamSizeManager` is a struct that helps coordinate multiple, potentially concurrent changes
/// to a VMO's stream size without needing to serialize the I/O of those operations. This is done by
/// maintaining queues of outstanding operations, allowing concurrent execution of the operations,
/// and then committing the stream size effects of those operations in a particular order. This idea
/// is similar to the re-order buffer in Tomasulo's algorithm.
///
/// There are 2 ordering queues: the read queue and the write queue. Both queues hold their
/// respective namesake operations as well as shrink operations.
///
/// Read operations are permitted to read up to the smallest outstanding stream size, which can be
/// found as the minimum of the current stream size and all shrink operations. Upon completion,
/// reads will always commit without blocking behind other operations.
///
/// Write operations may extend stream size. Upon completion, a write will block until it is the
/// head of the write queue if the smallest outstanding stream size is less than its target size.
///
/// Set size operations are treated differently, depending on whether the operation will expand or
/// shrink the stream size. When expanding, set size ops are treated as write operations of the same
/// target size (see above). When shrinking, set size ops are treated as shrink operations and will
/// block until it is the head if any read or write operations that operate beyond the target size
/// are queued in front of the set size.
#[guarded]
#[ref_counted]
#[derive(Recyclable)]
#[pin_data]
#[repr(C)]
pub struct StreamSizeManager {
#[pin]
#[mutex]
lock: KMutex<RawMutex>,
// These queues are usually very shallow, unless stream clients call many operations
// concurrently.
#[pin]
#[guarded_by(lock)]
write_q: DoublyLinkedList<*mut Operation, WriteQueueTag>,
#[pin]
#[guarded_by(lock)]
read_q: DoublyLinkedList<*mut Operation, ReadQueueTag>,
// `stream_size` is not guarded by a lock because the queues above maintain that only one
// operation can ever be mutating `stream_size` at any given point.
//
// Accessing this value should be done via `get_stream_size` and `set_stream_size`.
stream_size: AtomicU64,
}
impl StreamSizeManager {
/// Create a `StreamSizeManager` with its initial stream size set to `stream_size`.
pub fn create(stream_size: u64) -> Result<RefPtr<Self>, Status> {
fbl::pin_make_ref_counted!(Self {
lock <- KMutex::init(),
write_q <- ksync::KCell::pin_init(DoublyLinkedList::new()),
read_q <- ksync::KCell::pin_init(DoublyLinkedList::new()),
stream_size: AtomicU64::new(stream_size),
})
.map_err(|_: kalloc::AllocError| Status::NO_MEMORY)
}
/// Returns a reference to the stream size manager's mutex lock.
#[inline]
pub fn lock(&self) -> &KMutex<StreamSizeManagerLockClass, RawMutex> {
&self.lock
}
/// Returns the current stream size.
pub fn get_stream_size(&self) -> u64 {
// Loads from the operation the stream size must be ordered with the acquire ordering to
// ensure that all memory operations from the VMO (i.e. reads) after the load are not
// reordered before reading the stream size. Otherwise, reads from the VMO before acquiring
// stream size may not see data that was written to the VMO just before stream size was
// updated (via `SetStreamSize`).
self.stream_size.load(Ordering::Acquire)
}
/// Updates the stream size to a new value.
///
/// Note that this function should only be called by internal functions, as stream size should
/// only be modified by one operation at a time. This is enforced by the queues.
fn set_stream_size(&self, new_stream_size: u64) {
// Stores to the stream size must be ordered with release ordering to ensure that all memory
// operations (i.e. writes) to the VMO are visible *before* updating stream size. Readers
// must see valid data in the VMO if the region being read is within stream size. See
// `GetStreamSize` as well.
self.stream_size.store(new_stream_size, Ordering::Release);
}
fn push_write_queue_locked(
&self,
token: &mut LockToken<'_, StreamSizeManagerLockClass>,
op: &Operation,
) {
let mut guard = self.guard_lock_mut(token);
// SAFETY: `op` is pinned for the duration of the operation and inserted into `write_q`.
unsafe {
guard
.write_q_mut()
.get_unchecked_mut()
.push_back_raw(core::ptr::from_ref(op).cast_mut());
}
}
fn push_read_queue_locked(
&self,
token: &mut LockToken<'_, StreamSizeManagerLockClass>,
op: &Operation,
) {
let mut guard = self.guard_lock_mut(token);
// SAFETY: `op` is pinned for the duration of the operation and inserted into `read_q`.
unsafe {
guard
.read_q_mut()
.get_unchecked_mut()
.push_back_raw(core::ptr::from_ref(op).cast_mut());
}
}
fn dequeue_write_queue_locked(
&self,
token: &mut LockToken<'_, StreamSizeManagerLockClass>,
op: &Operation,
) {
let mut guard = self.guard_lock_mut(token);
// SAFETY: `op` is in `write_q`.
unsafe {
Self::dequeue_from_list::<WriteQueueTag>(guard.write_q_mut().get_unchecked_mut(), op);
}
}
fn dequeue_read_queue_locked(
&self,
token: &mut LockToken<'_, StreamSizeManagerLockClass>,
op: &Operation,
) {
let mut guard = self.guard_lock_mut(token);
// SAFETY: `op` is in `read_q`.
unsafe {
Self::dequeue_from_list::<ReadQueueTag>(guard.read_q_mut().get_unchecked_mut(), op);
}
}
/// Marks and registers the beginning of a read operation.
///
/// Returns the maximum size of the stream that should be read.
pub fn begin_read_locked(
&self,
token: &mut LockToken<'_, StreamSizeManagerLockClass>,
target_size: u64,
op: &Operation,
) -> u64 {
// Allow reads up to the smallest outstanding size.
// Other concurrent, in-flight operations may or may not complete before this read, so it
// is okay to be more conservative here and only read up to the guaranteed valid region.
let mut stream_size_limit = self.get_stream_size();
{
let guard = self.guard_lock(token);
for item in guard.read_q().iter() {
if item.get_type_locked(token) == OperationType::SetSize {
stream_size_limit =
core::cmp::min(item.get_size_locked(token), stream_size_limit);
}
}
}
stream_size_limit = core::cmp::min(target_size, stream_size_limit);
op.initialize(self, stream_size_limit, OperationType::Read, token);
self.push_read_queue_locked(token, op);
stream_size_limit
}
/// Marks and registers the beginning of a write operation.
///
/// If the write results in an expansion of the stream size, returns the previous stream size
/// from which the write expands. The gap from the previous stream size to where the write
/// begins likely needs to be zeroed out.
///
/// Notes:
/// * This function may block until other conflicting operations complete.
/// * This function may drop and reacquire the lock guarded by `guard`.
pub fn begin_write_locked(
&self,
guard: &mut Pin<&mut KMutexGuard<'_, StreamSizeManagerLockClass, RawMutex>>,
target_size: u64,
op: &Operation,
) -> Option<u64> {
let mut prev_stream_size = None;
op.initialize(self, target_size, OperationType::Write, guard.as_mut().token_mut());
self.push_write_queue_locked(guard.as_mut().token_mut(), op);
// Check if there are any set size operations in front of this that sets the stream size
// smaller than `target_size`.
let mut block_due_to_set = false;
for item in fbl::ReverseIterator::<*mut Operation, WriteQueueTag>::from_element(op).skip(1)
{
if item.get_type_locked(guard.token()) == OperationType::SetSize
&& item.get_size_locked(guard.token()) < target_size
{
block_due_to_set = true;
break;
}
}
// If this write can potentially create a scenario where it expands stream, block until it
// is the head of the queue.
if block_due_to_set || target_size > self.get_stream_size() {
self.block_until_head_locked(guard, op);
// Must re-read the stream size here, since `block_until_head_locked` would have dropped
// the lock, and stream size may have been modified by the operations in front of this
// one.
let cur_stream_size = self.get_stream_size();
if target_size > cur_stream_size {
prev_stream_size = Some(cur_stream_size);
}
}
prev_stream_size
}
/// Marks and registers the beginning of an append operation.
///
/// Notes:
/// * This function may block until other conflicting operations complete.
/// * This function may drop and reacquire the lock guarded by `guard`.
/// * `append_size` must be greater than 0.
pub fn begin_append_locked(
&self,
guard: &mut Pin<&mut KMutexGuard<'_, StreamSizeManagerLockClass, RawMutex>>,
append_size: usize,
op: &Operation,
) -> Result<(), Status> {
debug_assert!(append_size > 0);
// Temporarily initialize an `Append` operation with zero size until the size is known.
op.initialize(self, 0, OperationType::Append, guard.as_mut().token_mut());
self.push_write_queue_locked(guard.as_mut().token_mut(), op);
// Block until head if there are any of the following operations preceding this one:
// * Appends or writes that exceed the current stream size.
// * Set size
//
// Effectively, this checks for any stream size modifying operations.
let mut should_block = false;
// It's okay to read the stream size once here, since the lock is held. This means that
// stream size can only be increased if the front-most stream size modifying operation is
// an expanding write or append. Not re-reading stream size and seeing a potentially smaller
// stream size here is valid, since it will only pessimize (i.e. blocking until head) this
// operation for a very small number of cases within an extremely narrow timing window.
// There are no correctness issues with pessimization. Since the pessimizing case is so
// rare, prefer reading once over continuously re-reading the atomic in a loop.
let mut cur_stream_size = self.get_stream_size();
for item in fbl::ReverseIterator::<*mut Operation, WriteQueueTag>::from_element(op).skip(1)
{
let item_op_type = item.get_type_locked(guard.token());
if item_op_type == OperationType::SetSize
|| item_op_type == OperationType::Append
|| (item_op_type == OperationType::Write
&& item.get_size_locked(guard.token()) > cur_stream_size)
{
should_block = true;
break;
}
}
if should_block {
self.block_until_head_locked(guard, op);
// Must re-read the stream size here, since `block_until_head_locked` would have dropped
// the lock, and stream size may have been modified by the operations in front of this
// one.
cur_stream_size = self.get_stream_size();
}
// Using the previously read stream size. In the case where this operation blocked until
// it was the head, stream size was re-read with the lock held and with this operation at
// the head of the queue (no other stream size mutating operations can proceed before this).
// In all other cases, the previous loop verified that no stream size mutating operations
// are in front of this operation.
let Some(new_size) = cur_stream_size.checked_add(append_size as u64) else {
// Dequeue operation since this change should not be committed.
self.dequeue_operation_locked(op, guard.as_mut().token_mut());
return Err(Status::OUT_OF_RANGE);
};
*op.guard_phantom_lock_mut(guard.as_mut().token_mut()).size_mut() = new_size;
Ok(())
}
/// Marks and registers the beginning of an operation to set the stream size to a target size.
///
/// Note that this function may drop and reacquire the lock guarded by `guard`.
pub fn begin_set_stream_size_locked(
&self,
guard: &mut Pin<&mut KMutexGuard<'_, StreamSizeManagerLockClass, RawMutex>>,
target_size: u64,
op: &Operation,
) {
op.initialize(self, target_size, OperationType::SetSize, guard.as_mut().token_mut());
self.push_write_queue_locked(guard.as_mut().token_mut(), op);
self.push_read_queue_locked(guard.as_mut().token_mut(), op);
// Block until head if there are any of the following operations preceding this one:
// * Appends or writes that exceed either the current stream size or the target size.
// - If it exceeds the current stream size, the overlap is in the region in which the
// set size will zero content and the write will commit data.
// - If it exceeds the target size, the overlap is in the region in which the set size
// will invalidate pages/data and the write will commit data.
// * Reads that are reading at or beyond target size.
// * Set size
let mut should_block = false;
// It's okay to read the stream size once here, since the lock is held. This means that
// stream size can only be increased if the front-most stream size modifying operation is
// an expanding write or append. Not re-reading stream size and seeing a potentially smaller
// stream size here is valid, since it will only pessimize (i.e. blocking until head) this
// operation for a very small number of cases within an extremely narrow timing window.
// There are no correctness issues with pessimization. Since the pessimizing case is so
// rare, prefer reading once over continuously re-reading the atomic in a loop.
let cur_stream_size = self.get_stream_size();
for item in fbl::ReverseIterator::<*mut Operation, WriteQueueTag>::from_element(op).skip(1)
{
let item_op_type = item.get_type_locked(guard.token());
if item_op_type == OperationType::SetSize
|| item_op_type == OperationType::Append
|| (item_op_type == OperationType::Write
&& item.get_size_locked(guard.token())
> core::cmp::min(cur_stream_size, target_size))
{
should_block = true;
break;
}
}
if !should_block {
for item in
fbl::ReverseIterator::<*mut Operation, ReadQueueTag>::from_element(op).skip(1)
{
if item.get_type_locked(guard.token()) == OperationType::Read
&& item.get_size_locked(guard.token()) > target_size
{
should_block = true;
break;
}
}
}
if should_block {
self.block_until_head_locked(guard, op);
}
}
/// Blocks until the provided operation is at the head of the queue.
///
/// Note that this function will drop the lock guarded by `guard` while blocking and
/// reacquires the lock after.
fn block_until_head_locked(
&self,
guard: &mut Pin<&mut KMutexGuard<'_, StreamSizeManagerLockClass, RawMutex>>,
op: &Operation,
) {
debug_assert!(core::ptr::eq(op.parent, self));
if op.write_node.in_container() {
while op.is_valid_locked(guard.token()) {
let guard_lock = self.guard_lock(guard.token());
let is_front =
guard_lock.write_q().front().map(|f| core::ptr::eq(f, op)).unwrap_or(false);
if is_front {
break;
}
guard.as_mut().call_unlocked(|| {
let _ = op.ready_event.wait_infinite();
});
}
}
if op.read_node.in_container() {
while op.is_valid_locked(guard.token()) {
let guard_lock = self.guard_lock(guard.token());
let is_front =
guard_lock.read_q().front().map(|f| core::ptr::eq(f, op)).unwrap_or(false);
if is_front {
break;
}
guard.as_mut().call_unlocked(|| {
let _ = op.ready_event.wait_infinite();
});
}
}
}
fn commit_and_dequeue_operation_locked(
&self,
op: &Operation,
token: &mut LockToken<'_, StreamSizeManagerLockClass>,
) {
if !op.is_valid_locked(token) {
debug_assert!(!op.write_node.in_container());
debug_assert!(!op.read_node.in_container());
return;
}
match op.get_type_locked(token) {
OperationType::Write => {
let token_ref = &*token;
self.set_stream_size(core::cmp::max(
op.get_size_locked(token_ref),
self.get_stream_size(),
));
}
OperationType::Append | OperationType::SetSize => {
let token_ref = &*token;
self.set_stream_size(op.get_size_locked(token_ref));
}
OperationType::Read => {
// No-op
}
}
self.dequeue_operation_locked(op, token);
debug_assert!(!op.write_node.in_container());
debug_assert!(!op.read_node.in_container());
}
/// Dequeues an `Operation`. This must only be called internally, once an `Operation` is
/// committed or cancelled.
fn dequeue_operation_locked(
&self,
op: &Operation,
token: &mut LockToken<'_, StreamSizeManagerLockClass>,
) {
debug_assert!(op.is_valid_locked(token));
debug_assert!(core::ptr::eq(op.parent, self));
match op.get_type_locked(token) {
OperationType::Write | OperationType::Append => {
debug_assert!(op.write_node.in_container());
self.dequeue_write_queue_locked(token, op);
}
OperationType::Read => {
debug_assert!(op.read_node.in_container());
self.dequeue_read_queue_locked(token, op);
}
OperationType::SetSize => {
debug_assert!(op.read_node.in_container());
debug_assert!(op.write_node.in_container());
self.dequeue_write_queue_locked(token, op);
self.dequeue_read_queue_locked(token, op);
}
}
// Just in case, signal the ready event of `op` in case another thread is blocking on it.
//
// Note that this should never usually occur, since only the owning thread of the operation
// should be blocking or dequeueing.
op.ready_event.signal();
op.reset(token);
debug_assert!(!op.write_node.in_container());
debug_assert!(!op.read_node.in_container());
}
fn dequeue_from_list<Tag>(list: &mut DoublyLinkedList<*mut Operation, Tag>, op: &Operation)
where
Operation: DoublyLinkedListContainable<Operation, Tag>,
{
let is_head = list.front().map(|f| core::ptr::eq(f, op)).unwrap_or(false);
let next_op_ptr: Option<*const Operation> =
fbl::ForwardIterator::<*mut Operation, Tag>::from_element(op)
.nth(1)
.map(|next| next as *const Operation);
// SAFETY: `op` is guaranteed to be contained within `list`.
unsafe {
let _ = list.erase(op);
}
// If the current operation is now at the head of the list, signal to the next operation
// that it should wake to complete its task after this one finishes dequeueing.
if is_head && let Some(next_ptr) = next_op_ptr {
// SAFETY: `next_ptr` was in `list` and is now the new head.
unsafe {
(*next_ptr).ready_event.signal();
}
}
}
}
// C FFI exports for C++ callers
/// Creates a new `StreamSizeManager` via C FFI.
///
/// # Safety
///
/// `out_ptr` must be a valid, writable pointer to receive the raw `StreamSizeManager` pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_stream_size_manager_create(
stream_size: u64,
out_ptr: *mut *mut StreamSizeManager,
) -> zx_status_t {
match StreamSizeManager::create(stream_size) {
Ok(mgr) => {
// SAFETY: Caller guarantees `out_ptr` is valid and writable.
unsafe {
*out_ptr = RefPtr::into_raw(mgr).cast_mut();
}
ZX_OK
}
Err(status) => status.into_raw(),
}
}
/// Recycles a `StreamSizeManager` raw pointer when its reference count drops to zero.
///
/// # Safety
///
/// `stream_size_manager` must be a non-null, valid pointer to a `StreamSizeManager` previously
/// created by Rust.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_stream_size_manager_recycle(
stream_size_manager: *mut StreamSizeManager,
) {
// SAFETY: Caller guarantees `stream_size_manager` is a valid pointer whose refcount reached
// zero.
unsafe {
StreamSizeManager::recycle(NonNull::new_unchecked(stream_size_manager));
}
}
/// Returns the current stream size of `StreamSizeManager`.
///
/// # Safety
///
/// `stream_size_manager` must point to a live `StreamSizeManager` instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_stream_size_manager_get_stream_size(
stream_size_manager: *const StreamSizeManager,
) -> u64 {
// SAFETY: `stream_size_manager` is valid and non-null.
unsafe { (*stream_size_manager).get_stream_size() }
}
#[cfg(ktest)]
/// Unit tests for `StreamSizeManager`.
#[unittest::suite(name = "stream_size_manager")]
mod tests {
use super::{Operation, StreamSizeManager};
use unittest::{assert_ok, expect_eq, expect_false, expect_true, unwrap_ok};
/// Tests creating a StreamSizeManager and querying its initial size.
#[test]
fn create_and_get_size() {
let ssm = unwrap_ok!(StreamSizeManager::create(1024));
expect_eq!(ssm.get_stream_size(), 1024);
}
/// Tests begin_read_locked and committing a read operation.
#[test]
fn read_operation() {
let ssm = unwrap_ok!(StreamSizeManager::create(2048));
pin_init::stack_pin_init!(let op = Operation::init(&ssm));
ksync::lock!(let mut guard = ssm.lock().lock());
let limit = ssm.begin_read_locked(guard.as_mut().token_mut(), 4096, &op);
expect_eq!(limit, 2048);
expect_true!(op.is_valid_locked(guard.token()));
expect_eq!(op.get_size_locked(guard.token()), 2048);
op.commit_locked(guard.as_mut().token_mut());
expect_false!(op.is_valid_locked(guard.token()));
expect_eq!(ssm.get_stream_size(), 2048);
}
/// Tests begin_write_locked, shrinking the operation size, and committing.
#[test]
fn write_and_shrink_operation() {
let ssm = unwrap_ok!(StreamSizeManager::create(100));
pin_init::stack_pin_init!(let op = Operation::init(&ssm));
ksync::lock!(let mut guard = ssm.lock().lock());
let prev = ssm.begin_write_locked(&mut guard, 500, &op);
expect_true!(prev == Some(100));
expect_true!(op.is_valid_locked(guard.token()));
expect_eq!(op.get_size_locked(guard.token()), 500);
op.shrink_size_locked(guard.as_mut().token_mut(), 300);
expect_eq!(op.get_size_locked(guard.token()), 300);
op.commit_locked(guard.as_mut().token_mut());
expect_false!(op.is_valid_locked(guard.token()));
expect_eq!(ssm.get_stream_size(), 300);
}
/// Tests cancelling a write operation.
#[test]
fn write_cancel_operation() {
let ssm = unwrap_ok!(StreamSizeManager::create(100));
pin_init::stack_pin_init!(let op = Operation::init(&ssm));
ksync::lock!(let mut guard = ssm.lock().lock());
let _prev = ssm.begin_write_locked(&mut guard, 500, &op);
expect_true!(op.is_valid_locked(guard.token()));
op.cancel_locked(guard.as_mut().token_mut());
expect_false!(op.is_valid_locked(guard.token()));
expect_eq!(ssm.get_stream_size(), 100);
}
/// Tests begin_append_locked and committing an append operation.
#[test]
fn append_operation() {
let ssm = unwrap_ok!(StreamSizeManager::create(100));
pin_init::stack_pin_init!(let op = Operation::init(&ssm));
ksync::lock!(let mut guard = ssm.lock().lock());
assert_ok!(ssm.begin_append_locked(&mut guard, 50, &op));
expect_true!(op.is_valid_locked(guard.token()));
expect_eq!(op.get_size_locked(guard.token()), 150);
op.commit_locked(guard.as_mut().token_mut());
expect_false!(op.is_valid_locked(guard.token()));
expect_eq!(ssm.get_stream_size(), 150);
}
/// Tests begin_set_stream_size_locked and committing a set size operation.
#[test]
fn set_stream_size_operation() {
let ssm = unwrap_ok!(StreamSizeManager::create(100));
pin_init::stack_pin_init!(let op = Operation::init(&ssm));
ksync::lock!(let mut guard = ssm.lock().lock());
ssm.begin_set_stream_size_locked(&mut guard, 50, &op);
expect_true!(op.is_valid_locked(guard.token()));
expect_eq!(op.get_size_locked(guard.token()), 50);
op.commit_locked(guard.as_mut().token_mut());
expect_false!(op.is_valid_locked(guard.token()));
expect_eq!(ssm.get_stream_size(), 50);
}
/// Tests updating stream size as progress is made during a write operation.
#[test]
fn update_progress() {
let ssm = unwrap_ok!(StreamSizeManager::create(100));
pin_init::stack_pin_init!(let op = Operation::init(&ssm));
ksync::lock!(let mut guard = ssm.lock().lock());
let _prev = ssm.begin_write_locked(&mut guard, 500, &op);
op.update_stream_size_from_progress(250);
expect_eq!(ssm.get_stream_size(), 250);
op.commit_locked(guard.as_mut().token_mut());
expect_eq!(ssm.get_stream_size(), 500);
}
}