use core::sync::atomic::{AtomicBool, Ordering};
use ringbuf::traits::{Consumer, Observer, Producer, Split};
#[cfg(not(feature = "std"))]
use bevy_platform::prelude::String;
use crate::collector::ArcGc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RealtimeLoggerConfig {
pub max_message_length: usize,
pub num_slots: usize,
}
impl Default for RealtimeLoggerConfig {
fn default() -> Self {
Self {
max_message_length: 128,
num_slots: 32,
}
}
}
pub fn realtime_logger(config: RealtimeLoggerConfig) -> (RealtimeLogger, RealtimeLoggerMainThread) {
#[cfg(debug_assertions)]
let (mut debug_prod_1, debug_cons_1) = ringbuf::HeapRb::new(config.num_slots).split();
#[cfg(debug_assertions)]
let (debug_prod_2, debug_cons_2) = ringbuf::HeapRb::new(config.num_slots).split();
let (mut error_prod_1, error_cons_1) = ringbuf::HeapRb::new(config.num_slots).split();
let (error_prod_2, error_cons_2) = ringbuf::HeapRb::new(config.num_slots).split();
#[cfg(debug_assertions)]
for _ in 0..config.num_slots {
let mut slot = String::new();
slot.reserve_exact(config.max_message_length);
debug_prod_1.try_push(slot).unwrap();
}
for _ in 0..config.num_slots {
let mut slot = String::new();
slot.reserve_exact(config.max_message_length);
error_prod_1.try_push(slot).unwrap();
}
let shared_state = ArcGc::new(SharedState {
message_too_long_occured: AtomicBool::new(false),
not_enough_slots_occured: AtomicBool::new(false),
});
(
RealtimeLogger {
#[cfg(debug_assertions)]
debug_prod: debug_prod_2,
#[cfg(debug_assertions)]
debug_cons: debug_cons_1,
error_prod: error_prod_2,
error_cons: error_cons_1,
shared_state: ArcGc::clone(&shared_state),
max_msg_length: config.max_message_length,
},
RealtimeLoggerMainThread {
#[cfg(debug_assertions)]
debug_prod: debug_prod_1,
#[cfg(debug_assertions)]
debug_cons: debug_cons_2,
error_prod: error_prod_1,
error_cons: error_cons_2,
shared_state,
},
)
}
struct SharedState {
message_too_long_occured: AtomicBool,
not_enough_slots_occured: AtomicBool,
}
pub struct RealtimeLogger {
#[cfg(debug_assertions)]
debug_prod: ringbuf::HeapProd<String>,
#[cfg(debug_assertions)]
debug_cons: ringbuf::HeapCons<String>,
error_prod: ringbuf::HeapProd<String>,
error_cons: ringbuf::HeapCons<String>,
shared_state: ArcGc<SharedState>,
max_msg_length: usize,
}
impl RealtimeLogger {
pub fn max_message_length(&self) -> usize {
self.max_msg_length
}
pub fn available_debug_slots(&self) -> usize {
#[cfg(debug_assertions)]
return self.debug_cons.occupied_len();
#[cfg(not(debug_assertions))]
return 0;
}
pub fn available_error_slots(&self) -> usize {
self.error_cons.occupied_len()
}
#[allow(unused)]
pub fn try_debug(&mut self, message: &str) -> Result<(), RealtimeLogError> {
#[cfg(debug_assertions)]
{
if message.len() > self.max_msg_length {
self.shared_state
.message_too_long_occured
.store(true, Ordering::Relaxed);
return Err(RealtimeLogError::MessageTooLong);
}
let Some(mut slot) = self.debug_cons.try_pop() else {
self.shared_state
.not_enough_slots_occured
.store(true, Ordering::Relaxed);
return Err(RealtimeLogError::OutOfSlots);
};
slot.clear();
slot.push_str(message);
let _ = self.debug_prod.try_push(slot);
return Ok(());
}
#[cfg(not(debug_assertions))]
return Ok(());
}
#[allow(unused)]
pub fn try_debug_with(&mut self, f: impl FnOnce(&mut String)) -> Result<(), RealtimeLogError> {
#[cfg(debug_assertions)]
{
let Some(mut slot) = self.debug_cons.try_pop() else {
self.shared_state
.not_enough_slots_occured
.store(true, Ordering::Relaxed);
return Err(RealtimeLogError::OutOfSlots);
};
slot.clear();
(f)(&mut slot);
let _ = self.debug_prod.try_push(slot);
return Ok(());
}
#[cfg(not(debug_assertions))]
return Ok(());
}
pub fn try_error(&mut self, message: &str) -> Result<(), RealtimeLogError> {
if message.len() > self.max_msg_length {
self.shared_state
.message_too_long_occured
.store(true, Ordering::Relaxed);
return Err(RealtimeLogError::MessageTooLong);
}
let Some(mut slot) = self.error_cons.try_pop() else {
self.shared_state
.not_enough_slots_occured
.store(true, Ordering::Relaxed);
return Err(RealtimeLogError::OutOfSlots);
};
slot.clear();
slot.push_str(message);
let _ = self.error_prod.try_push(slot);
Ok(())
}
pub fn try_error_with(&mut self, f: impl FnOnce(&mut String)) -> Result<(), RealtimeLogError> {
let Some(mut slot) = self.error_cons.try_pop() else {
self.shared_state
.not_enough_slots_occured
.store(true, Ordering::Relaxed);
return Err(RealtimeLogError::OutOfSlots);
};
slot.clear();
(f)(&mut slot);
let _ = self.error_prod.try_push(slot);
Ok(())
}
}
pub struct RealtimeLoggerMainThread {
#[cfg(debug_assertions)]
debug_prod: ringbuf::HeapProd<String>,
#[cfg(debug_assertions)]
debug_cons: ringbuf::HeapCons<String>,
error_prod: ringbuf::HeapProd<String>,
error_cons: ringbuf::HeapCons<String>,
shared_state: ArcGc<SharedState>,
}
impl RealtimeLoggerMainThread {
pub fn flush(
&mut self,
mut log_error: impl FnMut(&str),
#[cfg(debug_assertions)] mut log_debug: impl FnMut(&str),
) {
if self
.shared_state
.message_too_long_occured
.swap(false, Ordering::Relaxed)
{
(log_error)("One or more realtime log messages were dropped because they were too long. Please increase message capacity.");
}
if self
.shared_state
.not_enough_slots_occured
.swap(false, Ordering::Relaxed)
{
(log_error)("One or more realtime log messages were dropped because the realtime logger ran out of slots. Please increase slot capacity.");
}
#[cfg(debug_assertions)]
for slot in self.debug_cons.pop_iter() {
(log_debug)(&slot);
let _ = self.debug_prod.try_push(slot).unwrap();
}
for slot in self.error_cons.pop_iter() {
(log_error)(&slot);
let _ = self.error_prod.try_push(slot).unwrap();
}
}
}
#[derive(Debug, Clone, Copy, thiserror::Error)]
pub enum RealtimeLogError {
#[error("There is not enough space to fit the message in the realtime log buffer")]
MessageTooLong,
#[error("The realtime log buffer is out of slots")]
OutOfSlots,
}