blob: c9b21b2f143e067ba5a1038f1e4408256ad1498a [file]
// Copyright 2025 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
//! Per-CPU SPSC ring buffer wrapper with dropped-record tracking.
use core::cell::UnsafeCell;
use core::mem::size_of;
use core::ptr::NonNull;
pub use percpu_writer_bindings::{DroppedRecordDurationEvent, DroppedRecordStats};
use spsc_buffer::{Buffer as SpscBuffer, NoOpAllocator, Reservation as SpscReservation};
use zerocopy::IntoBytes;
use zx_status::Status;
use super::KernelAspaceAllocator;
use crate::arch_rs::InterruptDisableGuard;
use crate::ktrace_rs::{
ArgumentHeader, ArgumentType, EventRecordHeader, EventType, LargeRecordHeader, RecordHeader,
RecordType, declare_interned_category, declare_interned_string,
};
use crate::platform_rs::timer::timer_current_boot_ticks;
declare_interned_category!(META_CAT, "kernel:meta", extern);
declare_interned_string!(DROP_STATS_REF, "drop_stats", extern);
declare_interned_string!(NUM_RECORDS_REF, "num_records", extern);
declare_interned_string!(NUM_BYTES_REF, "num_bytes", extern);
zr::static_assert!(size_of::<DroppedRecordStats>() == 32);
zr::static_assert!(size_of::<DroppedRecordDurationEvent>() == 56);
/// Serializes `stats` into a [`DroppedRecordDurationEvent`].
#[inline]
pub fn serialize_drop_stats(
stats: &DroppedRecordStats,
thread_ref: u8,
process_koid: u64,
thread_koid: u64,
) -> DroppedRecordDurationEvent {
// This method should only be called if we are currently tracking a run of dropped records.
debug_assert!(stats.has_dropped());
debug_assert!(stats.last_dropped != 0);
let record_size_words = (size_of::<DroppedRecordDurationEvent>() / size_of::<u64>()) as u16;
let mut header = EventRecordHeader::default();
header
.set_record_size(record_size_words)
.set_event_type(EventType::DurationComplete)
.set_arg_count(2)
.set_thread_ref(thread_ref)
.set_category_ref(META_CAT.label().id())
.set_name_ref(DROP_STATS_REF.id());
let mut num_dropped_arg =
ArgumentHeader::for_argument(NUM_RECORDS_REF.id(), 1, ArgumentType::Uint32);
num_dropped_arg.set_value_bits(stats.num_dropped);
let mut bytes_dropped_arg =
ArgumentHeader::for_argument(NUM_BYTES_REF.id(), 1, ArgumentType::Uint32);
bytes_dropped_arg.set_value_bits(stats.bytes_dropped);
DroppedRecordDurationEvent {
header: header.bits(),
start: stats.first_dropped,
process_id: process_koid,
thread_id: thread_koid,
num_dropped_arg: num_dropped_arg.bits(),
bytes_dropped_arg: bytes_dropped_arg.bits(),
end: stats.last_dropped,
}
}
/// `Reservation` encapsulates a pending write to the buffer.
///
/// Holding a shared reference to [`InterruptDisableGuard`] for the lifetime `'a` statically
/// ensures that interrupts remain disabled on the current CPU for the entire lifetime of this
/// reservation, preserving the single-writer invariant of each per-CPU buffer.
pub struct Reservation<'a> {
reservation: SpscReservation<'a>,
_irq_guard: &'a InterruptDisableGuard,
}
impl<'a> Reservation<'a> {
#[inline]
fn new(
reservation: SpscReservation<'a>,
irq_guard: &'a InterruptDisableGuard,
header: u64,
) -> Self {
let mut res = Self { reservation, _irq_guard: irq_guard };
res.write_word(header);
res
}
/// Writes a 64-bit word into the reservation.
#[inline]
pub fn write_word(&mut self, word: u64) {
let status = self.reservation.write(&word.to_ne_bytes());
debug_assert!(status.is_ok());
}
/// Writes `bytes` into the reservation, padding with trailing zeroes to an 8-byte boundary.
#[inline]
pub fn write_bytes(&mut self, bytes: &[u8]) {
// Write the data provided.
let status = self.reservation.write(bytes);
debug_assert!(status.is_ok());
// Write any padding bytes necessary.
let aligned_bytes = (bytes.len() + 7) & !7;
let num_zeros_to_write = aligned_bytes - bytes.len();
if num_zeros_to_write != 0 {
const ZERO: [u8; 8] = [0; 8];
let pad_status = self.reservation.write(&ZERO[..num_zeros_to_write]);
debug_assert!(pad_status.is_ok());
}
}
/// Commits the reservation, making the written record visible to readers.
#[inline]
pub fn commit(self) {
let status = self.reservation.commit();
debug_assert!(status.is_ok());
}
}
/// `percpu_writer::Buffer` wraps an [`SpscBuffer`] and adds functionality to track dropped trace
/// records.
#[repr(C, align(64))]
pub struct Buffer {
// Safety invariant: `buffer`'s backing storage is a live kernel address space region allocated
// by `KernelAspaceAllocator::allocate` in `new` and freed in `Drop`.
buffer: SpscBuffer<NoOpAllocator>,
/// Track stats of trace records dropped when the buffer is full.
///
/// Mutated exclusively by the owning CPU's producer path (`reserve` and `emit_drop_stats`)
/// with interrupts disabled.
drop_stats: UnsafeCell<DroppedRecordStats>,
process_koid: u64,
thread_koid: u64,
}
// SAFETY: `Buffer` owns its kernel address space backing storage (`self.buffer`) and can be safely
// sent across threads.
unsafe impl Send for Buffer {}
// SAFETY: Concurrent access to `Buffer` is split between a single per-CPU producer (serialized via
// `&InterruptDisableGuard` on the owning CPU, which mutates `drop_stats` through `UnsafeCell` and
// writes disjoint ring-buffer slots via `SpscBuffer::reserve_shared`) and a single consumer
// (`read` / `drain`), synchronized via `SpscBuffer`'s atomic read/write pointers.
unsafe impl Sync for Buffer {}
impl Drop for Buffer {
fn drop(&mut self) {
// Extract the raw storage pointer and reset the `SpscBuffer<NoOpAllocator>` view first,
// then free the underlying kernel address space mapping.
let (storage_ptr, _) = self.buffer.take_raw_parts();
// SAFETY: `storage_ptr` was allocated by `KernelAspaceAllocator::allocate` in `new`
// and is no longer in use because `self` is being dropped with exclusive `&mut self`.
unsafe {
KernelAspaceAllocator::free(NonNull::new(storage_ptr));
}
}
}
impl Buffer {
/// Maximum size of backing storage buffer (2 GiB), matching `spsc_buffer::Buffer`.
const MAX_STORAGE_SIZE: u32 = 1 << 31;
/// Allocates and initializes a new `Buffer` with backing storage of `size` bytes.
pub fn new(
size: u32,
buffer_name: &[u8],
process_koid: u64,
thread_koid: u64,
) -> Result<Self, Status> {
if size > Self::MAX_STORAGE_SIZE || !size.is_power_of_two() {
return Err(Status::INVALID_ARGS);
}
let storage =
KernelAspaceAllocator::allocate(size, buffer_name).ok_or(Status::NO_MEMORY)?;
// SAFETY: `storage` is non-null and points to a valid, zero-initialized kernel mapping of
// at least `size` bytes, where `size > 0`, `size <= MAX_STORAGE_SIZE`, and
// `size.is_power_of_two()`, and `Self` exclusively owns the backing mapping for the
// lifetime of `buffer`.
let buffer = unsafe { SpscBuffer::from_raw_parts(storage.as_ptr(), size as usize) };
Ok(Self {
buffer,
drop_stats: UnsafeCell::new(DroppedRecordStats::new()),
process_koid,
thread_koid,
})
}
/// Returns the configured size of the buffer in bytes.
#[inline]
pub const fn size(&self) -> u32 {
self.buffer.size()
}
/// Drains the underlying `SpscBuffer`.
pub fn drain(&self) -> Result<(), Status> {
self.buffer.drain()
}
/// Reads from the underlying `SpscBuffer`.
pub fn read<F>(&self, copy_fn: F, len: u32) -> Result<u32, Status>
where
F: FnMut(u32, &[u8]) -> Result<(), Status>,
{
self.buffer.read(copy_fn, len)
}
/// We interpose ourselves in the `reserve` path to ensure that we can emit a record containing
/// the dropped records statistics if we need to.
///
/// # Safety
///
/// - Producer calls ([`Self::reserve`] and [`Self::emit_drop_stats`]) on `self` must only be
/// invoked from the single CPU that owns this per-CPU buffer.
/// - No uncommitted [`Reservation`] for `self` may be live when this method is called.
pub unsafe fn reserve<'a>(
&'a self,
irq_guard: &'a InterruptDisableGuard,
header: u64,
) -> Result<Reservation<'a>, Status> {
// Compute the number of bytes we need to reserve from the provided fxt header.
let rec_hdr = RecordHeader::from(header);
let num_words = if matches!(rec_hdr.try_record_type(), Ok(RecordType::LargeBlob)) {
LargeRecordHeader::from(header).record_size()
} else {
u32::from(rec_hdr.record_size())
};
if num_words == 0 {
return Err(Status::INVALID_ARGS);
}
let size = num_words.checked_mul(size_of::<u64>() as u32).ok_or(Status::INVALID_ARGS)?;
// SAFETY: The caller guarantees this is the owning producer CPU with no live
// `Reservation`, `irq_guard` disables interrupts on this CPU, and readers never access
// `drop_stats`.
let drop_stats = unsafe { &mut *self.drop_stats.get() };
// If `first_dropped` is set to a value, then we are currently tracking a run of dropped
// trace records, so we need to emit a duration record containing that information. We
// could emit this record independently (with its own call to `SpscBuffer::reserve`), but
// this could lead to situations in which we thrash and emit multiple
// `DroppedRecordDurationEvent`s in a row. To avoid this, we attempt to reserve the
// desired size plus the size needed to store the `DroppedRecordDurationEvent` record, and
// then write the statistics into the first part of the reservation before returning it.
let has_dropped = drop_stats.has_dropped();
let mut total_size = size;
if has_dropped {
total_size = total_size.saturating_add(size_of::<DroppedRecordDurationEvent>() as u32);
}
// SAFETY: The caller guarantees this is the owning producer CPU with no live
// `Reservation`, and `irq_guard` disables interrupts on this CPU.
let mut res = match unsafe { self.buffer.reserve_shared(total_size) } {
Ok(res) => res,
Err(status) => {
// If the reservation failed, then we did not have enough space in this buffer,
// and the record we were attempting to write will be dropped. Add the `size` to
// the dropped record statistics. Notably, we do not add the `total_size`, because
// that may include the size of the `DroppedRecordDurationEvent`.
drop_stats.track(timer_current_boot_ticks().0, size);
return Err(status);
}
};
// If we need to write a dropped record duration event, do that here.
debug_assert!(has_dropped || total_size == size);
if has_dropped {
let record = serialize_drop_stats(drop_stats, 0, self.process_koid, self.thread_koid);
let write_status = res.write(record.as_bytes());
debug_assert!(write_status.is_ok());
// We've successfully written out the dropped record stats, so reset them for the next
// run.
drop_stats.reset();
}
Ok(Reservation::new(res, irq_guard, header))
}
/// Emit the dropped record stats to the trace buffer.
/// If we're not tracking a run of dropped records, this is a no-op.
///
/// # Safety
///
/// - Producer calls ([`Self::reserve`] and [`Self::emit_drop_stats`]) on `self` must only be
/// invoked from the single CPU that owns this per-CPU buffer.
/// - No uncommitted [`Reservation`] for `self` may be live when this method is called.
pub unsafe fn emit_drop_stats(&self, _irq_guard: &InterruptDisableGuard) -> Result<(), Status> {
// SAFETY: The caller guarantees this is the owning producer CPU with no live
// `Reservation`, `_irq_guard` disables interrupts on this CPU, and readers never access
// `drop_stats`.
let drop_stats = unsafe { &mut *self.drop_stats.get() };
if !drop_stats.has_dropped() {
debug_assert_eq!(drop_stats.last_dropped, 0);
return Ok(());
}
// Try to reserve a slot for the duration record. This will fail if there still isn't
// enough space in buffer to store the statistics.
// SAFETY: The caller guarantees this is the owning producer CPU with no live
// `Reservation`, and `_irq_guard` disables interrupts on this CPU.
let mut res =
unsafe { self.buffer.reserve_shared(size_of::<DroppedRecordDurationEvent>() as u32) }?;
let record = serialize_drop_stats(drop_stats, 0, self.process_koid, self.thread_koid);
let write_status = res.write(record.as_bytes());
debug_assert!(write_status.is_ok());
let commit_status = res.commit();
debug_assert!(commit_status.is_ok());
// We've successfully emitted a record containing statistics on the last run of dropped
// records. To prepare for the next run, we must reset the stats.
drop_stats.reset();
Ok(())
}
}
/// Unit tests for `percpu_writer::Buffer`.
#[cfg(ktest)]
#[unittest::suite(name = "percpu_writer_rust")]
mod tests {
use core::mem::size_of;
use fxt_layout::{MAGIC_VALUE, MetadataType, StringRecordHeader};
use unittest::{assert_eq, assert_err, assert_ok, assert_true, expect_ge, expect_le};
use zerocopy::FromBytes;
use zx_status::Status;
use super::{Buffer, DroppedRecordDurationEvent, timer_current_boot_ticks};
use crate::arch_rs::InterruptDisableGuard;
use crate::ktrace_rs::{RecordHeader, RecordType};
fn write_magic_number_record(
buffer: &Buffer,
irqd: &InterruptDisableGuard,
) -> Result<(), Status> {
let mut hdr = RecordHeader::new();
hdr.set_record_type(RecordType::Metadata).set_record_size(1);
let header =
hdr.bits() | ((MetadataType::TraceInfo as u64) << 16) | (u64::from(MAGIC_VALUE) << 24);
// SAFETY: Test owns `buffer` exclusively on the current CPU with `irqd` held and no
// uncommitted `Reservation` live.
let res = unsafe { buffer.reserve(irqd, header) }?;
res.commit();
Ok(())
}
fn write_string_record(
buffer: &Buffer,
irqd: &InterruptDisableGuard,
index: u16,
string: &[u8],
) -> Result<(), Status> {
let word_size = 1 + string.len().div_ceil(8) as u16;
let mut hdr = StringRecordHeader::default();
hdr.set_record_size(word_size).set_string_index(index).set_string_len(string.len() as u16);
// SAFETY: Test owns `buffer` exclusively on the current CPU with `irqd` held and no
// uncommitted `Reservation` live.
let mut res = unsafe { buffer.reserve(irqd, hdr.bits()) }?;
res.write_bytes(string);
res.commit();
Ok(())
}
/// Verifies dropped record tracking and `DroppedRecordDurationEvent` emission on `Buffer`.
#[test]
fn test_drop_stats() {
let buffer = Buffer::new(128, b"test-buffer", 0xAA, 0xBB).unwrap();
// Fill up the buffer.
{
let irqd = InterruptDisableGuard::new();
for _ in 0..(128 / 8) {
assert_ok!(write_magic_number_record(&buffer, &irqd));
}
}
const STRING_RECORD_SIZE: usize = 16;
// Reserving anything should fail.
let drop_start_time = timer_current_boot_ticks().0;
{
let irqd = InterruptDisableGuard::new();
assert_err!(write_string_record(&buffer, &irqd, 0, b"abcdefgh"), Status::NO_SPACE);
assert_err!(write_string_record(&buffer, &irqd, 0, b"abcdefgh"), Status::NO_SPACE);
assert_err!(write_string_record(&buffer, &irqd, 0, b"abcdefgh"), Status::NO_SPACE);
}
let drop_end_time = timer_current_boot_ticks().0;
// Clear out the buffer so we can write the dropped records record.
assert_ok!(buffer.drain());
{
let irqd = InterruptDisableGuard::new();
assert_ok!(write_string_record(&buffer, &irqd, 0, b"abcdefgh"));
}
let mut buf = [0u8; 128];
let bytes_read = buffer.read(
|offset, src| {
let start = offset as usize;
buf[start..start + src.len()].copy_from_slice(src);
Ok(())
},
128,
);
assert_true!(bytes_read.is_ok());
// We should only get the drop stats plus the record that triggered it.
let event_size = size_of::<DroppedRecordDurationEvent>();
assert_eq!(STRING_RECORD_SIZE + event_size, bytes_read.unwrap() as usize);
let record = DroppedRecordDurationEvent::read_from_bytes(&buf[..event_size]).unwrap();
expect_ge!(record.start, drop_start_time);
expect_le!(record.end, drop_end_time);
let num_dropped_value = record.num_dropped_arg >> 32;
let bytes_dropped_value = record.bytes_dropped_arg >> 32;
assert_eq!(num_dropped_value, 3u64);
assert_eq!(bytes_dropped_value, (3 * STRING_RECORD_SIZE) as u64);
assert_eq!(record.process_id, 0xAAu64);
assert_eq!(record.thread_id, 0xBBu64);
let record_data: [u8; 16] =
[0x22, 0, 0, 0, 8, 0, 0, 0, b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h'];
// After, we should get our record.
assert_true!(buf[event_size..event_size + 16] == record_data);
}
}