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