blob: 2c8c38f0320dfa6bc03206c5b7814585756cee16 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![no_std]
use unittest as _;
use arch_rs::{InterruptDisableGuard, curr_cpu_num, ints_disabled};
use core::cell::UnsafeCell;
use core::mem::{MaybeUninit, size_of};
use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering};
use core::{ffi, ptr, slice};
use kalloc::Box;
use kernel::thread::{FxtRef, ThreadPtr};
pub use kstring::declare_interned_category;
use kstring::declare_interned_string;
use kstring::interned_category::InternedCategory;
pub use kstring::interned_string::InternedString;
pub use platform_rs::{InstantBootTicks, timer_current_boot_ticks};
use spsc_buffer::{Buffer, NoOpAllocator, Reservation};
use zx_status::Status;
// Re-export the ktrace macros from the sub-crate.
pub use ktrace_macro::*;
#[allow(unused_extern_crates)]
extern crate self as ktrace_rs;
// LINT.IfChange(KTraceState)
#[repr(C)]
pub struct KTraceState {
pub categories_bitmask: AtomicU32,
pub writes_enabled: AtomicBool,
}
const _: () = assert!(size_of::<KTraceState>() == 8);
// LINT.ThenChange(//zircon/kernel/include/lib/ktrace.h:KTraceState)
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);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Context {
Thread = 0,
Cpu = 1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventType {
Instant = 0,
Counter = 1,
DurationBegin = 2,
DurationEnd = 3,
DurationComplete = 4,
FlowBegin = 8,
FlowStep = 9,
FlowEnd = 10,
}
/// The value of a trace argument.
pub enum ArgValue<'a> {
Null,
Bool(bool),
Int32(i32),
Uint32(u32),
Int64(i64),
Uint64(u64),
Double(f64),
String(&'a str),
Pointer(usize),
Koid(u64),
}
impl<'a> From<bool> for ArgValue<'a> {
fn from(v: bool) -> Self {
ArgValue::Bool(v)
}
}
impl<'a> From<i32> for ArgValue<'a> {
fn from(v: i32) -> Self {
ArgValue::Int32(v)
}
}
impl<'a> From<u32> for ArgValue<'a> {
fn from(v: u32) -> Self {
ArgValue::Uint32(v)
}
}
impl<'a> From<i64> for ArgValue<'a> {
fn from(v: i64) -> Self {
ArgValue::Int64(v)
}
}
impl<'a> From<u64> for ArgValue<'a> {
fn from(v: u64) -> Self {
ArgValue::Uint64(v)
}
}
impl<'a> From<f64> for ArgValue<'a> {
fn from(v: f64) -> Self {
ArgValue::Double(v)
}
}
impl<'a> From<&'a str> for ArgValue<'a> {
fn from(v: &'a str) -> Self {
ArgValue::String(v)
}
}
pub struct Argument<'a> {
name: &'static InternedString,
value: ArgValue<'a>,
}
impl<'a> Argument<'a> {
pub fn new(name: &'static InternedString, value: impl Into<ArgValue<'a>>) -> Self {
Self { name, value: value.into() }
}
/// Returns the size of this argument in 64-bit words.
fn size_words(&self) -> usize {
match &self.value {
ArgValue::Null | ArgValue::Bool(_) | ArgValue::Int32(_) | ArgValue::Uint32(_) => 1,
ArgValue::Int64(_)
| ArgValue::Uint64(_)
| ArgValue::Double(_)
| ArgValue::Pointer(_)
| ArgValue::Koid(_) => 2,
ArgValue::String(s) => 1 + (s.len() + 7) / 8,
}
}
fn write(&self, res: &mut KTraceReservation<'_>) -> Result<(), Status> {
let name_id = self.name.id() as u64;
let mut header = 0u64;
header |= (name_id & 0xffff) << 16; // NameRef
match &self.value {
ArgValue::Null => {
header |= 0u64; // ArgumentType::kNull (0)
res.write_word(header)?;
}
ArgValue::Int32(v) => {
header |= 1u64; // ArgumentType::kInt32 (1)
header |= ((*v as u32) as u64) << 32;
res.write_word(header)?;
}
ArgValue::Uint32(v) => {
header |= 2u64; // ArgumentType::kUint32 (2)
header |= (*v as u64) << 32;
res.write_word(header)?;
}
ArgValue::Int64(v) => {
header |= 3u64; // ArgumentType::kInt64 (3)
res.write_word(header)?;
res.write_word(*v as u64)?;
}
ArgValue::Uint64(v) => {
header |= 4u64; // ArgumentType::kUint64 (4)
res.write_word(header)?;
res.write_word(*v)?;
}
ArgValue::Double(v) => {
header |= 5u64; // ArgumentType::kDouble (5)
res.write_word(header)?;
res.write_word(v.to_bits())?;
}
ArgValue::String(s) => {
header |= 6u64; // ArgumentType::kString (6)
let string_len = s.len();
header |= (((string_len + 7) / 8) as u64) << 4; // ArgumentSize in words
header |= (string_len as u64) << 32; // String length in bytes
res.write_word(header)?;
res.write_bytes(s.as_bytes())?;
}
ArgValue::Pointer(v) => {
header |= 7u64; // ArgumentType::kPointer (7)
res.write_word(header)?;
res.write_word(*v as u64)?;
}
ArgValue::Koid(v) => {
header |= 8u64; // ArgumentType::kKoid (8)
res.write_word(header)?;
res.write_word(*v)?;
}
ArgValue::Bool(v) => {
header |= 9u64; // ArgumentType::kBool (9)
header |= ((*v as u64) & 1) << 32;
res.write_word(header)?;
}
}
Ok(())
}
}
pub struct KTraceScope<'a> {
category: &'static InternedCategory,
name: &'static InternedString,
timestamp: InstantBootTicks,
context: Context,
args: &'a [Argument<'a>],
}
impl<'a> KTraceScope<'a> {
#[inline(never)]
#[cold]
pub fn begin(
category: &'static InternedCategory,
name: &'static InternedString,
context: Context,
args: &'a [Argument<'a>],
) -> Self {
let timestamp = timer_current_boot_ticks();
Self { category, name, timestamp, context, args }
}
}
impl<'a> Drop for KTraceScope<'a> {
#[inline(never)]
#[cold]
fn drop(&mut self) {
let end_time = timer_current_boot_ticks();
let ktrace = KTrace::get_instance();
ktrace.emit_event(
EventType::DurationComplete,
self.category,
self.name,
self.timestamp,
self.context,
Some(end_time.0 as u64),
self.args,
);
}
}
// LINT.IfChange(DroppedRecordDurationEvent)
#[repr(C)]
struct DroppedRecordDurationEvent {
header: u64,
start: InstantBootTicks,
process_id: u64,
thread_id: u64,
num_dropped_arg: u64,
bytes_dropped_arg: u64,
end: InstantBootTicks,
}
const _: () = assert!(size_of::<DroppedRecordDurationEvent>() == 56);
// LINT.ThenChange(//zircon/kernel/lib/percpu_writer/include/lib/percpu_writer/buffer.h:DroppedRecordDurationEvent)
// LINT.IfChange(DroppedRecordStats)
/// This struct keeps track of the duration, number, and size of trace records dropped when the
/// buffer is full. These statistics are emitted to the trace buffer as a duration as soon as
/// space is available to do so, at which point the values are reset to 0, or false in the case
/// of has_dropped.
#[repr(C)]
#[derive(Default, Debug, Clone)]
pub struct DroppedRecordStats {
pub first_dropped: InstantBootTicks,
pub last_dropped: InstantBootTicks,
/// By storing num_dropped and bytes_dropped in 32-bit values, we ensure that they can each
/// be stored in a single 64-bit word in the FXT record we emit when space is available.
pub num_dropped: u32,
pub bytes_dropped: u32,
pub has_dropped: bool,
}
const _: () = assert!(size_of::<DroppedRecordStats>() == 32);
// LINT.ThenChange(//zircon/kernel/lib/percpu_writer/include/lib/percpu_writer/buffer.h:DroppedRecordStats)
impl DroppedRecordStats {
pub fn reset(&mut self) {
self.first_dropped = InstantBootTicks(0);
self.last_dropped = InstantBootTicks(0);
self.num_dropped = 0;
self.bytes_dropped = 0;
self.has_dropped = false;
}
pub fn track(&mut self, now: InstantBootTicks, size: u32) {
if !self.has_dropped {
self.first_dropped = now;
self.has_dropped = true;
}
self.last_dropped = now;
self.num_dropped = self.num_dropped.wrapping_add(1);
self.bytes_dropped = self.bytes_dropped.wrapping_add(size);
}
pub fn has_dropped(&self) -> bool {
self.has_dropped
}
}
/// A Rust implementation of `percpu_writer::Buffer` that wraps a static reference to an existing
/// `spsc_buffer::Buffer` and tracks dropped trace records.
pub struct KTraceBuffer {
buffer: &'static mut Buffer<NoOpAllocator>,
pub drop_stats: &'static mut DroppedRecordStats,
size: u32,
cpu_ref_header_entry: u16,
pub process_koid: u64,
pub thread_koid: u64,
}
// SAFETY: Access to the per-CPU buffers is synchronized by the caller (typically via disabling
// interrupts).
unsafe impl Send for KTraceBuffer {}
unsafe impl Sync for KTraceBuffer {}
impl KTraceBuffer {
/// Constructs a `KTraceBuffer` from a static reference to an existing `spsc_buffer::Buffer`,
/// a static reference to `DroppedRecordStats`, and its metadata.
pub fn new(
buffer: &'static mut Buffer<NoOpAllocator>,
drop_stats: &'static mut DroppedRecordStats,
cpu_ref_header_entry: u16,
process_koid: u64,
thread_koid: u64,
) -> Self {
let size = buffer.size();
Self { buffer, drop_stats, size, cpu_ref_header_entry, process_koid, thread_koid }
}
/// Returns the size of the backing storage.
pub fn size(&self) -> u32 {
self.size
}
/// Drains the underlying Buffer.
pub fn drain(&self) -> Result<(), Status> {
self.buffer.drain()
}
/// Copies `len` bytes out of the buffer using the provided `copy_fn`.
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)
}
/// Reserves a block of the given size in the buffer, interposing to write dropped record
/// statistics if any were tracked.
pub fn reserve(&mut self, header: u64) -> Result<KTraceReservation<'_>, Status> {
debug_assert!(ints_disabled());
// Compute the number of bytes we need to reserve from the provided fxt header.
let record_type = (header & 0xf) as u32;
let num_words = if record_type == 15 {
// Large record
((header >> 4) & 0xffffffff) as u32
} else {
// Normal record
((header >> 4) & 0xfff) as u32
};
let size = num_words * 8;
let mut total_size = size;
let event = if self.drop_stats.has_dropped() {
total_size += size_of::<DroppedRecordDurationEvent>() as u32;
Some(self.serialize_drop_stats())
} else {
None
};
match self.buffer.reserve(total_size) {
Err(status) => {
let now = timer_current_boot_ticks();
self.drop_stats.track(now, size);
Err(status)
}
Ok(mut res) => {
if let Some(event) = event {
// SAFETY: DroppedRecordDurationEvent is repr(C) and has a defined binary
// layout.
let event_bytes = unsafe {
slice::from_raw_parts(
ptr::from_ref(&event).cast::<u8>(),
size_of::<DroppedRecordDurationEvent>(),
)
};
res.write(event_bytes)?;
self.drop_stats.reset();
}
Ok(KTraceReservation::new(res, header))
}
}
}
/// Emit the dropped record stats to the trace buffer if we're currently tracking them.
pub fn emit_drop_stats(&mut self) -> Result<(), Status> {
debug_assert!(ints_disabled());
if !self.drop_stats.has_dropped() {
return Ok(());
}
// Serialize the event first so we release the borrow on self before calling reserve.
let event = self.serialize_drop_stats();
let mut res = self.buffer.reserve(size_of::<DroppedRecordDurationEvent>() as u32)?;
// SAFETY: DroppedRecordDurationEvent is repr(C) and has a defined binary layout.
let event_bytes = unsafe {
slice::from_raw_parts(
ptr::from_ref(&event).cast::<u8>(),
size_of::<DroppedRecordDurationEvent>(),
)
};
res.write(event_bytes)?;
res.commit()?;
// Reset the fields directly rather than calling reset_drop_stats() to avoid
// borrowing the entire self while res is in scope.
self.drop_stats.reset();
Ok(())
}
/// Resets the dropped records statistics to their initial values.
pub fn reset_drop_stats(&mut self) {
self.drop_stats.reset();
}
fn serialize_drop_stats(&self) -> DroppedRecordDurationEvent {
let mut header = 4u64; // RecordType::kEvent (4)
let record_size_words = (size_of::<DroppedRecordDurationEvent>() / 8) as u64;
header |= record_size_words << 4; // RecordSize
header |= 4u64 << 16; // EventType::kDurationComplete (4)
header |= 2u64 << 20; // ArgumentCount = 2
header |= (self.cpu_ref_header_entry as u64) << 24;
header |= (META_CAT.label().id() as u64) << 32;
header |= (DROP_STATS_REF.id() as u64) << 48;
// Pack the arguments.
// In FXT:
// ArgumentType::kUint32 is 2
// ArgumentSize is 1 word (8 bytes)
// NameRef is packed into bits 16..31
// Value is packed into bits 32..63
let mut num_dropped_arg = 2u64;
num_dropped_arg |= 1u64 << 4;
num_dropped_arg |= (NUM_RECORDS_REF.id() as u64) << 16;
num_dropped_arg |= (self.drop_stats.num_dropped as u64) << 32;
let mut bytes_dropped_arg = 2u64;
bytes_dropped_arg |= 1u64 << 4;
bytes_dropped_arg |= (NUM_BYTES_REF.id() as u64) << 16;
bytes_dropped_arg |= (self.drop_stats.bytes_dropped as u64) << 32;
DroppedRecordDurationEvent {
header,
start: self.drop_stats.first_dropped,
process_id: self.process_koid,
thread_id: self.thread_koid,
num_dropped_arg,
bytes_dropped_arg,
end: self.drop_stats.last_dropped,
}
}
}
/// KTraceReservation encapsulates a pending write to the buffer.
#[derive(Debug)]
pub struct KTraceReservation<'a> {
reservation: Reservation<'a>,
}
impl<'a> KTraceReservation<'a> {
fn new(reservation: Reservation<'a>, header: u64) -> Self {
let mut this = Self { reservation };
let _ = this.write_word(header);
this
}
/// Writes a single 64-bit word into the reservation.
pub fn write_word(&mut self, word: u64) -> Result<(), Status> {
debug_assert!(ints_disabled());
self.reservation.write(&word.to_ne_bytes())
}
/// Writes a byte slice into the reservation, padding to an 8-byte boundary.
pub fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), Status> {
debug_assert!(ints_disabled());
self.reservation.write(bytes)?;
let num_bytes = bytes.len();
let aligned_bytes = (num_bytes + 7) & !7;
let num_zeros_to_write = aligned_bytes - num_bytes;
if num_zeros_to_write > 0 {
let zero = [0u8; 8];
self.reservation.write(&zero[..num_zeros_to_write])?;
}
Ok(())
}
/// Commits the reservation, making it visible to the reader.
pub fn commit(self) -> Result<(), Status> {
debug_assert!(ints_disabled());
self.reservation.commit()
}
}
/// A pure Rust implementation of KTrace.
pub struct KTrace {
/// Reference to the shared C++ KTraceState.
// TODO(https://fxbug.dev/517305548): This should be made a direct allocation, and not a
// reference, once the C++ implementation is removed.
state: &'static KTraceState,
/// A heap-allocated slice of atomic pointers to per-CPU buffers.
/// Allocated once at boot time, and accessed completely lock-free on the hot path by CPU ID.
buffers: Box<[AtomicPtr<KTraceBuffer>]>,
}
// SAFETY: KTrace is a global singleton. Access to the per-CPU buffers is synchronized.
unsafe impl Sync for KTrace {}
unsafe impl Send for KTrace {}
#[repr(transparent)]
struct KTraceSingleton(UnsafeCell<MaybeUninit<KTrace>>);
unsafe impl Sync for KTraceSingleton {}
unsafe impl Send for KTraceSingleton {}
static INSTANCE: KTraceSingleton = KTraceSingleton(UnsafeCell::new(MaybeUninit::uninit()));
impl KTrace {
/// Retrieve the global instance of KTrace.
pub fn get_instance() -> &'static Self {
// SAFETY: KTrace must be initialized during kernel boot.
unsafe { &*INSTANCE.0.get().cast::<KTrace>() }
}
/// Returns the raw pointer to the KTraceBuffer for the given CPU.
pub fn get_buffer(&self, cpu: usize) -> *mut KTraceBuffer {
self.buffers[cpu].load(Ordering::Acquire)
}
/// Reserves a slot of memory to write a record into.
///
/// # Safety
///
/// This method MUST be invoked with interrupts disabled to enforce the single-writer invariant.
pub unsafe fn reserve(&self, header: u64) -> Result<KTraceReservation<'_>, Status> {
debug_assert!(ints_disabled());
if !self.writes_enabled() {
return Err(Status::BAD_STATE);
}
// Direct, lock-free indexing into the slice, followed by loading the atomic pointer!
let ptr = self.buffers[curr_cpu_num() as usize].load(Ordering::Acquire);
if ptr.is_null() {
return Err(Status::BAD_STATE);
}
let buf = unsafe { &mut *ptr };
buf.reserve(header)
}
/// Returns true if writes are currently enabled.
pub fn writes_enabled(&self) -> bool {
self.state.writes_enabled.load(Ordering::Acquire)
}
/// Returns the categories bitmask.
pub fn categories_bitmask(&self) -> u32 {
self.state.categories_bitmask.load(Ordering::Acquire)
}
/// Returns true if the given category is enabled.
pub fn is_category_enabled(&self, category: &InternedCategory) -> bool {
let category_index = category.index();
if category_index == InternedCategory::INVALID_INDEX {
return false;
}
let bitmask = self.categories_bitmask();
(bitmask & (1 << category_index)) != 0
}
/// Low-level helper to write an FXT kernel object record.
///
/// This is not inlined to reduce code size at the instrumentation sites.
#[inline(never)]
#[cold]
pub fn emit_kernel_object_outlined(
&self,
koid: u64,
obj_type: u32,
name: &InternedString,
args: &[Argument<'_>],
) {
let _guard = InterruptDisableGuard::new();
if !self.writes_enabled() {
return;
}
let base_size = 2; // Header, KOID
let args_size: usize = args.iter().map(|a| a.size_words()).sum();
let total_size_words = base_size + args_size;
if total_size_words > 0xfff {
return;
}
let mut header = 7u64; // RecordType::kKernelObject (7)
header |= (total_size_words as u64) << 4; // RecordSize
header |= (obj_type as u64) << 16; // ObjectType
header |= (name.id() as u64) << 24; // NameStringRef
header |= (args.len() as u64) << 40; // ArgumentCount
if let Ok(mut res) = unsafe { self.reserve(header) } {
let _ = res.write_word(koid);
for arg in args {
let _ = arg.write(&mut res);
}
let _ = res.commit();
}
}
/// Low-level helper to write a generic FXT event record.
///
/// This is not inlined to reduce code size at the instrumentation sites.
#[inline(never)]
#[cold]
pub fn emit_event(
&self,
event_type: EventType,
category: &InternedCategory,
name: &InternedString,
timestamp: InstantBootTicks,
context: Context,
content: Option<u64>,
args: &[Argument<'_>],
) {
let _guard = InterruptDisableGuard::new();
if !self.writes_enabled() {
return;
}
// 1. Get the process/thread KOIDs for the context.
let (process_koid, thread_koid) = match context {
Context::Thread => {
// SAFETY: ktrace is initialized and running after threading has been initialized.
let FxtRef { pid, tid } = unsafe { ThreadPtr::current().fxt_ref() };
(pid, tid)
}
Context::Cpu => {
let cpu = curr_cpu_num() as usize;
let ptr = self.buffers[cpu].load(Ordering::Acquire);
if ptr.is_null() {
return;
}
let buf = unsafe { &*ptr };
(buf.process_koid, buf.thread_koid)
}
};
// 2. Calculate the record size.
let base_size = 4; // Header, Timestamp, Process KOID, Thread KOID
let content_size = if content.is_some() { 1 } else { 0 };
let args_size: usize = args.iter().map(|a| a.size_words()).sum();
let total_size_words = base_size + content_size + args_size;
if total_size_words > 0xfff {
return;
}
// 3. Construct the header.
let mut header = 4u64; // RecordType::kEvent (4)
header |= (total_size_words as u64) << 4; // RecordSize
header |= (event_type as u32 as u64) << 16; // EventType
header |= (args.len() as u64) << 20; // ArgumentCount
header |= (category.label().id() as u64) << 32; // CategoryStringRef
header |= (name.id() as u64) << 48; // NameStringRef
// 4. Reserve space and write the record.
if let Ok(mut res) = unsafe { self.reserve(header) } {
let _ = res.write_word(timestamp.0 as u64);
let _ = res.write_word(process_koid);
let _ = res.write_word(thread_koid);
for arg in args {
let _ = arg.write(&mut res);
}
if let Some(c) = content {
let _ = res.write_word(c);
}
let _ = res.commit();
}
}
}
/// Initializes the global KTrace instance with the given number of CPU buffers.
///
/// # Safety
///
/// This must be called at most once during kernel boot.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ktrace_init(num_buffers: u32, state_ptr: *mut ffi::c_void) -> i32 {
if num_buffers == 0 || state_ptr.is_null() {
return Status::INVALID_ARGS.into_raw();
}
// SAFETY: The caller guarantees that state_ptr points to a valid KTraceState instance
// which has static storage duration (lives forever) and is safe to access concurrently
// (since its fields are atomic).
let state = unsafe { &*state_ptr.cast::<KTraceState>() };
let buffers = match Box::<[AtomicPtr<KTraceBuffer>]>::try_new_zeroed_slice(num_buffers as usize)
{
Ok(b) => b,
Err(_) => return Status::NO_MEMORY.into_raw(),
};
let ktrace = KTrace { state, buffers };
unsafe {
let slot = INSTANCE.0.get();
ptr::write(slot.cast::<KTrace>(), ktrace);
}
Status::OK.into_raw()
}
/// Initializes the KTraceBuffer for a specific CPU using a pointer to the C++ SpscBuffer.
///
/// # Safety
///
/// - `spsc_buffer_ptr` must point to a valid C++ `SpscBuffer` instance
/// which is binary-compatible with `spsc_buffer::Buffer<NoOpAllocator>`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ktrace_init_cpu_buffer(
cpu_num: u32,
spsc_buffer_ptr: *mut ffi::c_void,
drop_stats_ptr: *mut ffi::c_void,
process_koid: u64,
thread_koid: u64,
cpu_ref_header_entry: u16,
) -> i32 {
let ktrace = KTrace::get_instance();
if cpu_num >= ktrace.buffers.len() as u32 {
return Status::INVALID_ARGS.into_raw();
}
// SAFETY: The caller guarantees the pointers are valid, 'static, and binary-compatible with
// their respective Rust types.
let (buffer, drop_stats) = unsafe {
(
&mut *spsc_buffer_ptr.cast::<Buffer<NoOpAllocator>>(),
&mut *drop_stats_ptr.cast::<DroppedRecordStats>(),
)
};
// Allocate the KTraceBuffer on the heap.
let buf =
KTraceBuffer::new(buffer, drop_stats, cpu_ref_header_entry, process_koid, thread_koid);
let boxed_buf = match Box::try_new(buf) {
Ok(b) => b,
Err(_) => return Status::NO_MEMORY.into_raw(),
};
let raw_ptr = Box::into_raw(boxed_buf);
// Store the pointer atomically.
let old_ptr = ktrace.buffers[cpu_num as usize].swap(raw_ptr, Ordering::AcqRel);
if !old_ptr.is_null() {
// If there was a previous buffer, reclaim and drop it.
unsafe {
let _ = Box::from_raw(old_ptr);
}
}
Status::OK.into_raw()
}
/// KTrace tests
#[cfg(all(not(gcc), ktest))]
#[unittest::test_suite(name = "ktrace_rust")]
mod tests {
use arch_rs::{InterruptDisableGuard, curr_cpu_num, max_num_cpus};
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use core::{ffi, ptr};
use kernel::thread::{FxtRef, ThreadPtr};
use spsc_buffer::Buffer;
use unittest::{
assert_eq, assert_ok, expect_eq, expect_false, expect_ne, expect_true, unwrap_ok,
};
declare_interned_category!(MEMORY_CAT, "kernel:memory", extern);
declare_interned_category!(SCHED_CAT, "kernel:sched", extern);
declare_interned_category!(CONTENTION_CAT, "kernel:contention", extern);
declare_interned_category!(IPC_CAT, "kernel:ipc", extern);
declare_interned_category!(IRQ_CAT, "kernel:irq", extern);
/// Initialization and size/metadata attributes.
#[test]
fn init_and_size() {
let mut storage = [0u8; 256];
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let leaked_ref = unsafe { &mut *ptr::from_mut(&mut inner_buf) };
let mut stats = DroppedRecordStats::default();
let leaked_stats = unsafe { &mut *ptr::from_mut(&mut stats) };
let kbuf = KTraceBuffer::new(leaked_ref, leaked_stats, 1, 100, 200);
expect_eq!(kbuf.size(), 256);
expect_eq!(kbuf.process_koid, 100);
expect_eq!(kbuf.thread_koid, 200);
}
/// Reservation, writing words, and committing.
#[test]
fn test_write() {
let _guard = InterruptDisableGuard::new();
let mut storage = [0u8; 256];
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let leaked_ref = unsafe { &mut *ptr::from_mut(&mut inner_buf) };
let mut stats = DroppedRecordStats::default();
let leaked_stats = unsafe { &mut *ptr::from_mut(&mut stats) };
let mut kbuf = KTraceBuffer::new(leaked_ref, leaked_stats, 1, 100, 200);
// Reserve 16 bytes (2 words).
let header = 4u64 | (2u64 << 4);
let mut res = unwrap_ok!(kbuf.reserve(header));
// Write a word (8 bytes).
assert_ok!(res.write_word(0xabcdef0123456789));
assert_ok!(res.commit());
// Read it back.
let mut read_bytes = [0u8; 16];
let read_len = unwrap_ok!(kbuf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
16,
));
assert_eq!(read_len, 16);
expect_eq!(u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap()), header);
expect_eq!(u64::from_ne_bytes(read_bytes[8..16].try_into().unwrap()), 0xabcdef0123456789);
}
// Verifies tracking of dropped records and their subsequent serialization when space becomes
// available.
//
/// Dropped record tracking and serialization.
#[test]
fn dropped_record_tracking() {
let _guard = InterruptDisableGuard::new();
let mut storage = [0u8; 128]; // small buffer
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let leaked_ref = unsafe { &mut *ptr::from_mut(&mut inner_buf) };
let mut stats = DroppedRecordStats::default();
let leaked_stats = unsafe { &mut *ptr::from_mut(&mut stats) };
let mut kbuf = KTraceBuffer::new(leaked_ref, leaked_stats, 1, 100, 200);
// Reserve almost all space.
// 128 bytes total. Let's reserve 96 bytes (12 words).
let header = 4u64 | (12u64 << 4);
let mut res = unwrap_ok!(kbuf.reserve(header));
assert_ok!(res.write_bytes(&[0u8; 88]));
assert_ok!(res.commit());
// Now, try to reserve 64 bytes (8 words). This should fail because there are only 32 bytes
// left.
let header2 = 4u64 | (8u64 << 4);
expect_true!(kbuf.reserve(header2).err() == Some(Status::NO_SPACE));
// This failed reservation should have been tracked!
expect_true!(kbuf.drop_stats.has_dropped());
expect_eq!(kbuf.drop_stats.num_dropped, 1);
expect_eq!(kbuf.drop_stats.bytes_dropped, 64);
// Now, drain the buffer to free all space.
assert_ok!(kbuf.drain());
// Now, reserve 16 bytes (2 words).
// Since first_dropped was set, this reservation should successfully write the 56-byte
// dropped records duration event first!
let header3 = 4u64 | (2u64 << 4);
let mut res3 = unwrap_ok!(kbuf.reserve(header3));
assert_ok!(res3.write_bytes(&[0u8; 8]));
assert_ok!(res3.commit());
// The dropped stats should have been reset!
expect_false!(kbuf.drop_stats.has_dropped());
expect_eq!(kbuf.drop_stats.num_dropped, 0);
expect_eq!(kbuf.drop_stats.bytes_dropped, 0);
// Let's read the buffer content.
let mut read_bytes = [0u8; 72];
let read_len = unwrap_ok!(kbuf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
72,
));
expect_eq!(read_len, 72);
// Verify the DroppedRecordDurationEvent header:
let event_header = u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap());
expect_eq!(event_header & 0xf, 4);
expect_eq!((event_header >> 4) & 0xfff, 7);
expect_eq!((event_header >> 16) & 0xf, 4);
expect_eq!((event_header >> 20) & 0xf, 2);
expect_eq!((event_header >> 24) & 0xff, 1);
expect_eq!((event_header >> 32) & 0xffff, u64::from(META_CAT.label().id()));
expect_eq!((event_header >> 48) & 0xffff, u64::from(DROP_STATS_REF.id()));
// Verify process_koid (100) and thread_koid (200)
expect_eq!(u64::from_ne_bytes(read_bytes[16..24].try_into().unwrap()), 100);
expect_eq!(u64::from_ne_bytes(read_bytes[24..32].try_into().unwrap()), 200);
// Verify the two arguments:
let num_dropped_arg = u64::from_ne_bytes(read_bytes[32..40].try_into().unwrap());
expect_eq!(num_dropped_arg & 0xf, 2);
expect_eq!((num_dropped_arg >> 4) & 0xfff, 1);
expect_eq!((num_dropped_arg >> 16) & 0xffff, u64::from(NUM_RECORDS_REF.id()));
expect_eq!((num_dropped_arg >> 32) & 0xffffffff, 1);
let bytes_dropped_arg = u64::from_ne_bytes(read_bytes[40..48].try_into().unwrap());
expect_eq!(bytes_dropped_arg & 0xf, 2);
expect_eq!((bytes_dropped_arg >> 16) & 0xffff, u64::from(NUM_BYTES_REF.id()));
expect_eq!((bytes_dropped_arg >> 32) & 0xffffffff, 64);
// Verify the reservation header3:
let res_header = u64::from_ne_bytes(read_bytes[56..64].try_into().unwrap());
expect_eq!(res_header, header3);
}
/// Direct invocation of KTraceBuffer::emit_drop_stats.
#[test]
fn emit_drop_stats() {
let _guard = InterruptDisableGuard::new();
let mut storage = [0u8; 128];
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let leaked_ref = unsafe { &mut *ptr::from_mut(&mut inner_buf) };
let mut stats = DroppedRecordStats::default();
let leaked_stats = unsafe { &mut *ptr::from_mut(&mut stats) };
let mut kbuf = KTraceBuffer::new(leaked_ref, leaked_stats, 1, 100, 200);
// 1. Force a failed reservation to track a dropped record.
let header = 4u64 | (32u64 << 4);
expect_true!(kbuf.reserve(header).err() == Some(Status::NO_SPACE));
expect_true!(kbuf.drop_stats.has_dropped());
expect_eq!(kbuf.drop_stats.num_dropped, 1);
expect_eq!(kbuf.drop_stats.bytes_dropped, 256);
// 2. Call emit_drop_stats directly.
assert_ok!(kbuf.emit_drop_stats());
expect_false!(kbuf.drop_stats.has_dropped());
expect_eq!(kbuf.drop_stats.num_dropped, 0);
expect_eq!(kbuf.drop_stats.bytes_dropped, 0);
// 3. Read and verify the event.
let mut read_bytes = [0u8; 56];
let read_len = unwrap_ok!(kbuf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
56,
));
expect_eq!(read_len, 56);
let event_header = u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap());
expect_eq!(event_header & 0xf, 4);
expect_eq!((event_header >> 4) & 0xfff, 7);
expect_eq!((event_header >> 16) & 0xf, 4);
expect_eq!((event_header >> 20) & 0xf, 2);
expect_eq!((event_header >> 24) & 0xff, 1);
expect_eq!((event_header >> 32) & 0xffff, u64::from(META_CAT.label().id()));
expect_eq!((event_header >> 48) & 0xffff, u64::from(DROP_STATS_REF.id()));
expect_eq!(u64::from_ne_bytes(read_bytes[16..24].try_into().unwrap()), 100);
expect_eq!(u64::from_ne_bytes(read_bytes[24..32].try_into().unwrap()), 200);
let num_dropped_arg = u64::from_ne_bytes(read_bytes[32..40].try_into().unwrap());
expect_eq!(num_dropped_arg & 0xf, 2);
expect_eq!((num_dropped_arg >> 16) & 0xffff, u64::from(NUM_RECORDS_REF.id()));
expect_eq!((num_dropped_arg >> 32) & 0xffffffff, 1);
let bytes_dropped_arg = u64::from_ne_bytes(read_bytes[40..48].try_into().unwrap());
expect_eq!(bytes_dropped_arg & 0xf, 2);
expect_eq!((bytes_dropped_arg >> 16) & 0xffff, u64::from(NUM_BYTES_REF.id()));
expect_eq!((bytes_dropped_arg >> 32) & 0xffffffff, 256);
}
// Verifies the full global lifecycle of KTrace: initialization, category bitmasks, CPU buffer
// allocation, and high-level tracing macro execution.
//
/// Validate full global lifecycle.
#[test]
fn global_lifecycle() {
let _guard = InterruptDisableGuard::new();
// Initialize indices of the categories we're testing.
META_CAT.set_index(0, kstring::interned_category::InternedCategory::INVALID_INDEX);
MEMORY_CAT.set_index(1, kstring::interned_category::InternedCategory::INVALID_INDEX);
SCHED_CAT.set_index(2, kstring::interned_category::InternedCategory::INVALID_INDEX);
CONTENTION_CAT.set_index(3, kstring::interned_category::InternedCategory::INVALID_INDEX);
IPC_CAT.set_index(4, kstring::interned_category::InternedCategory::INVALID_INDEX);
IRQ_CAT.set_index(5, kstring::interned_category::InternedCategory::INVALID_INDEX);
// 1. Initialize the global KTrace instance with the system CPU count buffers and a local
// mock state.
let num_cpus = max_num_cpus();
let mut local_state = KTraceState {
categories_bitmask: AtomicU32::new(0),
writes_enabled: AtomicBool::new(false),
};
let local_state_ptr = ptr::from_mut(&mut local_state).cast::<ffi::c_void>();
let status = unsafe { rust_ktrace_init(num_cpus, local_state_ptr) };
expect_eq!(status, 0);
let ktrace = KTrace::get_instance();
// 2. Verify initial states.
expect_false!(ktrace.writes_enabled());
expect_eq!(ktrace.categories_bitmask(), 0);
expect_false!(ktrace.is_category_enabled(&META_CAT));
expect_false!(ktrace.is_category_enabled(&IRQ_CAT));
// 3. Test writes_enabled.
local_state.writes_enabled.store(true, Ordering::Release);
expect_true!(ktrace.writes_enabled());
local_state.writes_enabled.store(false, Ordering::Release);
expect_false!(ktrace.writes_enabled());
// 4. Test categories_bitmask.
let mask = (1 << MEMORY_CAT.index()) | (1 << CONTENTION_CAT.index());
local_state.categories_bitmask.store(mask, Ordering::Release);
expect_eq!(ktrace.categories_bitmask(), mask);
expect_false!(ktrace.is_category_enabled(&META_CAT));
expect_true!(ktrace.is_category_enabled(&MEMORY_CAT));
expect_false!(ktrace.is_category_enabled(&SCHED_CAT));
expect_true!(ktrace.is_category_enabled(&CONTENTION_CAT));
expect_false!(ktrace.is_category_enabled(&IPC_CAT));
// 5. Test CPU buffer initialization and reserve.
let mut storage = [0u8; 256];
let mut inner_buf = unsafe { Buffer::from_raw_parts(storage.as_mut_ptr(), storage.len()) };
let inner_buf_ptr = ptr::from_mut(&mut inner_buf).cast::<ffi::c_void>();
let mut stats = DroppedRecordStats::default();
let stats_ptr = ptr::from_mut(&mut stats).cast::<ffi::c_void>();
// Initialize current CPU buffer.
let cpu = curr_cpu_num();
let init_status = unsafe {
rust_ktrace_init_cpu_buffer(
cpu, // cpu_num
inner_buf_ptr,
stats_ptr,
100, // process_koid
200, // thread_koid
1, // cpu_ref_header_entry
)
};
expect_eq!(init_status, 0);
// If writes are disabled, reserving should fail.
let header = 4u64 | (2u64 << 4); // Event with 2 words
expect_true!(unsafe { ktrace.reserve(header) }.err() == Some(Status::BAD_STATE));
// Enable writes.
local_state.writes_enabled.store(true, Ordering::Release);
// Now reserve should succeed!
let mut res = unwrap_ok!(unsafe { ktrace.reserve(header) });
assert_ok!(res.write_word(0x1234567890abcdef));
assert_ok!(res.commit());
// Read and verify the written record from the current CPU buffer.
let ptr = ktrace.get_buffer(cpu as usize);
expect_false!(ptr.is_null());
let buf = unsafe { &*ptr };
let mut read_bytes = [0u8; 16];
let read_len = unwrap_ok!(buf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
16,
));
expect_eq!(read_len, 16);
expect_eq!(u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap()), header);
expect_eq!(u64::from_ne_bytes(read_bytes[8..16].try_into().unwrap()), 0x1234567890abcdef);
// 6. Test Rust macros!
let mask = (1 << META_CAT.index()) | (1 << MEMORY_CAT.index());
local_state.categories_bitmask.store(mask, Ordering::Release);
expect_true!(ktrace.is_category_enabled(&META_CAT));
// Let's emit an instant event using the macro!
instant!(META_CAT, "my_event", "arg1" => 42i32, "arg2" => "hello");
// Now let's read the record back and verify it!
let mut read_bytes = [0u8; 128];
let read_len = unwrap_ok!(buf.read(
|offset, src| {
read_bytes[offset as usize..offset as usize + src.len()].copy_from_slice(src);
Ok(())
},
56,
));
expect_eq!(read_len, 56);
// Verify Header Word
let header_word = u64::from_ne_bytes(read_bytes[0..8].try_into().unwrap());
expect_eq!(header_word & 0xf, 4);
expect_eq!((header_word >> 4) & 0xfff, 7);
expect_eq!((header_word >> 16) & 0xf, 0);
expect_eq!((header_word >> 20) & 0xf, 2);
expect_eq!((header_word >> 32) & 0xffff, u64::from(META_CAT.label().id()));
let name_id = (header_word >> 48) & 0xffff;
expect_ne!(name_id, 0);
// Verify Timestamp (word 1)
let timestamp_word = u64::from_ne_bytes(read_bytes[8..16].try_into().unwrap());
expect_ne!(timestamp_word, 0);
// Verify KOIDs (words 2 & 3)
let process_koid = u64::from_ne_bytes(read_bytes[16..24].try_into().unwrap());
let thread_koid = u64::from_ne_bytes(read_bytes[24..32].try_into().unwrap());
// SAFETY: Tests run after threading has been initialized.
let FxtRef { pid: expected_proc, tid: expected_thread } =
unsafe { ThreadPtr::current().fxt_ref() };
expect_eq!(process_koid, expected_proc);
expect_eq!(thread_koid, expected_thread);
// Verify Argument 1 ("arg1" => 42i32) (word 4)
let arg1_header = u64::from_ne_bytes(read_bytes[32..40].try_into().unwrap());
expect_eq!(arg1_header & 0xf, 1);
expect_eq!((arg1_header >> 32) & 0xffffffff, 42);
let arg1_name_id = (arg1_header >> 16) & 0xffff;
expect_ne!(arg1_name_id, 0);
// Verify Argument 2 ("arg2" => "hello") (words 5 & 6)
let arg2_header = u64::from_ne_bytes(read_bytes[40..48].try_into().unwrap());
expect_eq!(arg2_header & 0xf, 6);
expect_eq!((arg2_header >> 4) & 0xf, 1);
expect_eq!((arg2_header >> 32) & 0xffffffff, 5);
let arg2_name_id = (arg2_header >> 16) & 0xffff;
expect_ne!(arg2_name_id, 0);
let arg2_val = &read_bytes[48..56];
expect_true!(&arg2_val[0..5] == b"hello");
expect_true!(&arg2_val[5..8] == &[0, 0, 0]);
}
}
//
// Zircon C++ kernel-tests FFI tests.
//
/// Test-only FFI helper to write a single word record from Rust.
///
/// # Safety
///
/// This must be called with interrupts disabled.
#[cfg(ktest)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ktrace_test_interop(header: u64, val: u64) -> i32 {
let ktrace = KTrace::get_instance();
// SAFETY: The caller guarantees interrupts are disabled.
if let Ok(mut res) = unsafe { ktrace.reserve(header) } {
let _ = res.write_word(val);
let _ = res.commit();
0
} else {
-1
}
}
/// Test-only FFI helper to exercise all ktrace macros from Rust.
///
/// # Safety
///
/// This must be called with interrupts disabled and KTrace active.
#[cfg(ktest)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ktrace_test_macros() {
// Exercise each macro. We'll write specific, distinguishable events.
instant!("kernel:meta", "rust_instant", Context::Thread, "val" => 101u32);
duration_begin!("kernel:meta", "rust_duration", Context::Thread, "val" => 102u32);
duration_end!("kernel:meta", "rust_duration", Context::Thread, "val" => 103u32);
counter!("kernel:meta", "rust_counter", 104u64, "val" => 105u32);
flow_begin!("kernel:meta", "rust_flow", 106u64, "val" => 107u32);
flow_step!("kernel:meta", "rust_flow", 106u64, "val" => 108u32);
flow_end!("kernel:meta", "rust_flow", 106u64, "val" => 109u32);
complete!("kernel:meta", "rust_complete", InstantBootTicks(110i64), "val" => 111u32);
kernel_object!("kernel:meta", 112u64, 1u32, "rust_kernel_obj", "val" => 113u32);
kernel_object_always!(114u64, 2u32, "rust_kernel_obj_always", "val" => 115u32);
}