Skip to main content

firewheel_core/
log.rs

1use bevy_platform::sync::Arc;
2use core::sync::atomic::{AtomicBool, Ordering};
3use ringbuf::traits::{Consumer, Observer, Producer, Split};
4
5#[cfg(not(feature = "std"))]
6use bevy_platform::prelude::String;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct RealtimeLoggerConfig {
12    /// The capacity of each message slot. This determines the maximum length a
13    /// single log message can be.
14    ///
15    /// It is highly recommended to have this be at least `128`.
16    ///
17    /// By default this is set to `128`.
18    pub max_message_length: usize,
19
20    /// The number of slots available. This determines how many log messages
21    /// can be queued at once.
22    ///
23    /// By default this is set to `32`.
24    pub num_slots: usize,
25}
26
27impl Default for RealtimeLoggerConfig {
28    fn default() -> Self {
29        Self {
30            max_message_length: 128,
31            num_slots: 32,
32        }
33    }
34}
35
36pub fn realtime_logger(config: RealtimeLoggerConfig) -> (RealtimeLogger, RealtimeLoggerMainThread) {
37    #[cfg(debug_assertions)]
38    let (mut debug_prod_1, debug_cons_1) = ringbuf::HeapRb::new(config.num_slots).split();
39    #[cfg(debug_assertions)]
40    let (debug_prod_2, debug_cons_2) = ringbuf::HeapRb::new(config.num_slots).split();
41
42    let (mut error_prod_1, error_cons_1) = ringbuf::HeapRb::new(config.num_slots).split();
43    let (error_prod_2, error_cons_2) = ringbuf::HeapRb::new(config.num_slots).split();
44
45    #[cfg(debug_assertions)]
46    for _ in 0..config.num_slots {
47        let mut slot = String::new();
48        slot.reserve_exact(config.max_message_length);
49
50        debug_prod_1.try_push(slot).unwrap();
51    }
52
53    for _ in 0..config.num_slots {
54        let mut slot = String::new();
55        slot.reserve_exact(config.max_message_length);
56
57        error_prod_1.try_push(slot).unwrap();
58    }
59
60    let shared_state = Arc::new(SharedState {
61        message_too_long_occurred: AtomicBool::new(false),
62        not_enough_slots_occurred: AtomicBool::new(false),
63    });
64
65    (
66        RealtimeLogger {
67            #[cfg(debug_assertions)]
68            debug_prod: debug_prod_2,
69            #[cfg(debug_assertions)]
70            debug_cons: debug_cons_1,
71            error_prod: error_prod_2,
72            error_cons: error_cons_1,
73            shared_state: Arc::clone(&shared_state),
74            max_msg_length: config.max_message_length,
75        },
76        RealtimeLoggerMainThread {
77            #[cfg(debug_assertions)]
78            debug_prod: debug_prod_1,
79            #[cfg(debug_assertions)]
80            debug_cons: debug_cons_2,
81            error_prod: error_prod_1,
82            error_cons: error_cons_2,
83            shared_state,
84        },
85    )
86}
87
88struct SharedState {
89    message_too_long_occurred: AtomicBool,
90    not_enough_slots_occurred: AtomicBool,
91}
92
93/// A helper used for realtime-safe logging on the audio thread.
94pub struct RealtimeLogger {
95    #[cfg(debug_assertions)]
96    debug_prod: ringbuf::HeapProd<String>,
97    #[cfg(debug_assertions)]
98    debug_cons: ringbuf::HeapCons<String>,
99
100    error_prod: ringbuf::HeapProd<String>,
101    error_cons: ringbuf::HeapCons<String>,
102
103    shared_state: Arc<SharedState>,
104
105    max_msg_length: usize,
106}
107
108impl RealtimeLogger {
109    /// The allocated capacity for each message slot.
110    pub fn max_message_length(&self) -> usize {
111        self.max_msg_length
112    }
113
114    /// Returns the number of slots that are available for debug messages.
115    ///
116    /// This will always return `0` when compiled without debug assertions.
117    pub fn available_debug_slots(&self) -> usize {
118        #[cfg(debug_assertions)]
119        return self.debug_cons.occupied_len();
120        #[cfg(not(debug_assertions))]
121        return 0;
122    }
123
124    /// Returns the number of slots that are available for error messages.
125    pub fn available_error_slots(&self) -> usize {
126        self.error_cons.occupied_len()
127    }
128
129    /// Log the given debug message.
130    ///
131    /// *NOTE*, avoid using this method in the final release of your node.
132    /// This is only meant for debugging purposes while developing.
133    ///
134    /// This will do nothing when compiled without debug assertions.
135    #[allow(unused)]
136    pub fn try_debug(&mut self, message: &str) -> Result<(), RealtimeLogError> {
137        #[cfg(debug_assertions)]
138        {
139            if message.len() > self.max_msg_length {
140                self.shared_state
141                    .message_too_long_occurred
142                    .store(true, Ordering::Relaxed);
143                return Err(RealtimeLogError::MessageTooLong);
144            }
145
146            let Some(mut slot) = self.debug_cons.try_pop() else {
147                self.shared_state
148                    .not_enough_slots_occurred
149                    .store(true, Ordering::Relaxed);
150                return Err(RealtimeLogError::OutOfSlots);
151            };
152
153            slot.clear();
154            slot.push_str(message);
155
156            let _ = self.debug_prod.try_push(slot);
157        }
158
159        Ok(())
160    }
161
162    /// Log a debug message into the given string.
163    ///
164    /// This string is guaranteed to be empty and have an allocated capacity
165    /// of at least [`RealtimeLogger::max_message_length`].
166    ///
167    /// *NOTE*, avoid using this method in the final release of your node.
168    /// This is only meant for debugging purposes while developing.
169    ///
170    /// This will do nothing when compiled without debug assertions.
171    #[allow(unused)]
172    pub fn try_debug_with(&mut self, f: impl FnOnce(&mut String)) -> Result<(), RealtimeLogError> {
173        #[cfg(debug_assertions)]
174        {
175            let Some(mut slot) = self.debug_cons.try_pop() else {
176                self.shared_state
177                    .not_enough_slots_occurred
178                    .store(true, Ordering::Relaxed);
179                return Err(RealtimeLogError::OutOfSlots);
180            };
181
182            slot.clear();
183
184            (f)(&mut slot);
185
186            let _ = self.debug_prod.try_push(slot);
187        }
188
189        Ok(())
190    }
191
192    /// Log the given error message.
193    pub fn try_error(&mut self, message: &str) -> Result<(), RealtimeLogError> {
194        if message.len() > self.max_msg_length {
195            self.shared_state
196                .message_too_long_occurred
197                .store(true, Ordering::Relaxed);
198            return Err(RealtimeLogError::MessageTooLong);
199        }
200
201        let Some(mut slot) = self.error_cons.try_pop() else {
202            self.shared_state
203                .not_enough_slots_occurred
204                .store(true, Ordering::Relaxed);
205            return Err(RealtimeLogError::OutOfSlots);
206        };
207
208        slot.clear();
209        slot.push_str(message);
210
211        let _ = self.error_prod.try_push(slot);
212
213        Ok(())
214    }
215
216    /// Log an error message into the given string.
217    ///
218    /// This string is guaranteed to be empty and have an allocated capacity
219    /// of at least [`RealtimeLogger::max_message_length`].
220    pub fn try_error_with(&mut self, f: impl FnOnce(&mut String)) -> Result<(), RealtimeLogError> {
221        let Some(mut slot) = self.error_cons.try_pop() else {
222            self.shared_state
223                .not_enough_slots_occurred
224                .store(true, Ordering::Relaxed);
225            return Err(RealtimeLogError::OutOfSlots);
226        };
227
228        slot.clear();
229
230        (f)(&mut slot);
231
232        let _ = self.error_prod.try_push(slot);
233
234        Ok(())
235    }
236}
237
238/// The main thread counterpart to a [`RealtimeLogger`].
239pub struct RealtimeLoggerMainThread {
240    #[cfg(debug_assertions)]
241    debug_prod: ringbuf::HeapProd<String>,
242    #[cfg(debug_assertions)]
243    debug_cons: ringbuf::HeapCons<String>,
244
245    error_prod: ringbuf::HeapProd<String>,
246    error_cons: ringbuf::HeapCons<String>,
247
248    shared_state: Arc<SharedState>,
249}
250
251impl RealtimeLoggerMainThread {
252    /// Flush the queued log messages.
253    pub fn flush(
254        &mut self,
255        mut log_error: impl FnMut(&str),
256        #[allow(unused)] mut log_debug: impl FnMut(&str),
257    ) {
258        if self
259            .shared_state
260            .message_too_long_occurred
261            .swap(false, Ordering::Relaxed)
262        {
263            (log_error)(
264                "One or more realtime log messages were dropped because they were too long. Please increase message capacity.",
265            );
266        }
267        if self
268            .shared_state
269            .not_enough_slots_occurred
270            .swap(false, Ordering::Relaxed)
271        {
272            (log_error)(
273                "One or more realtime log messages were dropped because the realtime logger ran out of slots. Please increase slot capacity.",
274            );
275        }
276
277        #[cfg(debug_assertions)]
278        for slot in self.debug_cons.pop_iter() {
279            (log_debug)(&slot);
280            self.debug_prod.try_push(slot).unwrap();
281        }
282
283        for slot in self.error_cons.pop_iter() {
284            (log_error)(&slot);
285            self.error_prod.try_push(slot).unwrap();
286        }
287    }
288}
289
290#[derive(Debug, Clone, Copy, thiserror::Error)]
291pub enum RealtimeLogError {
292    /// There is not enough space to fit the message in the realtime log buffer.
293    #[error("There is not enough space to fit the message in the realtime log buffer")]
294    MessageTooLong,
295    #[error("The realtime log buffer is out of slots")]
296    OutOfSlots,
297}