firewheel_core/
log.rs

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