logwise/
interval.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2/*!
3Defines our Interval types.
4
5These represent 2 paired log values, such as a start and end time.
6*/
7
8use crate::Level;
9use crate::context::Context;
10use crate::global_logger::global_loggers;
11use crate::log_record::LogRecord;
12use std::sync::atomic::{AtomicU64, Ordering};
13
14/// Global counter for generating unique profile interval IDs.
15///
16/// This counter is used to correlate BEGIN and END log messages for profile
17/// intervals, especially useful when profiling nested operations.
18static PROFILE_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
19
20/// A performance warning interval that tracks execution time.
21///
22/// `PerfwarnInterval` measures the duration between its creation and destruction (drop),
23/// logging the elapsed time as a performance warning. The duration is also accumulated
24/// into the current task's statistics for aggregated reporting.
25///
26/// # Usage
27///
28/// This type is typically not created directly. Instead, use the `perfwarn!` or
29/// `perfwarn_begin!` macros which properly set up the interval with source location
30/// information.
31///
32/// # Example
33///
34/// ```rust
35/// logwise::declare_logging_domain!();
36/// # fn main() {
37/// # fn perform_expensive_operation() {}
38/// // Using the perfwarn! macro (recommended)
39/// logwise::perfwarn!("database_query", {
40///     perform_expensive_operation();
41/// });
42///
43/// // Using perfwarn_begin! for more control
44/// let interval = logwise::perfwarn_begin!("network_request");
45/// perform_expensive_operation();
46/// // Duration is logged when interval drops
47/// drop(interval);
48/// # }
49/// ```
50///
51/// # Scaling
52///
53/// Use the [`scale`](PerfwarnInterval::scale) method to adjust the reported duration when
54/// only a portion of the measured time is relevant:
55///
56/// ```rust
57/// logwise::declare_logging_domain!();
58/// # fn main() {
59/// # fn perform_expensive_operation() {}
60/// let mut interval = logwise::perfwarn_begin!("partial_operation");
61/// perform_expensive_operation();
62/// // Report only 20% of the actual duration
63/// interval.scale(0.2);
64/// # }
65/// ```
66#[derive(Debug)]
67pub struct PerfwarnInterval {
68    label: &'static str,
69    start: crate::sys::Instant,
70    scale: f32,
71}
72
73impl PerfwarnInterval {
74    /**
75        Creates a new interval.
76
77        Do not use this manually, instead use the `perfwarn!` macro, or if you need to access the interval directly, use `perfwarn_begin!`.
78    */
79    #[inline]
80    pub fn new(label: &'static str, time: crate::sys::Instant) -> Self {
81        Self {
82            label,
83            start: time,
84            scale: 1.0,
85        }
86    }
87
88    #[inline]
89    #[doc(hidden)]
90    ///internal implementation detail
91    pub fn log_timestamp(&self, record: &mut LogRecord) {
92        let time = crate::sys::Instant::now();
93        let duration = time.duration_since(self.start);
94        record.log_owned(format!("[{:?}] ", duration));
95    }
96
97    /**
98        Cause the reported time interval to be scaled by the amount.
99
100        Consider a case where we're warning about "some subset of the interval".  For example,
101        let's say by tweaking constants we can get a 20% speedup.  Warning about the entire interval
102        would be misleading.  Instead, we can scale the interval by 0.2 to reflect the subset.
103    */
104    pub fn scale(&mut self, scale: f32) {
105        self.scale = scale;
106    }
107}
108
109impl Drop for PerfwarnInterval {
110    fn drop(&mut self) {
111        let end_time = crate::sys::Instant::now();
112        let duration = end_time.duration_since(self.start);
113        let ctx = Context::current();
114        ctx._add_task_interval(self.label, duration);
115
116        let mut record = LogRecord::new(Level::PerfWarn);
117        let ctx = Context::current();
118        ctx._log_prelude(&mut record);
119        record.log("PERWARN: END ");
120        record.log_time_since(end_time);
121
122        record.log(self.label);
123        record.log(" ");
124        let scaled_duration = duration.mul_f32(self.scale);
125        record.log_owned(format!("[interval took {:?}] ", scaled_duration));
126        let global_loggers = global_loggers();
127        for logger in global_loggers {
128            logger.finish_log_record(record.clone());
129        }
130    }
131}
132
133/*
134boilerplate notes.
135
1361.  Copy, clone, no.  We don't want to copy the context?
1372.  PartialEq, Ord, etc.  No, we don't want to compare these.
1383.  Default, no, we don't want to create these without a start time.
1394.  display, not really.
1405.  From/Into, I think we want to avoid creating this without a line number.
1416.  AsRef/AsMut, there's really nothing to desugar to
1427.  Deref, similar
1438.  Send/Sync, Probably?
144 */
145
146#[cfg(test)]
147mod tests {
148    #[cfg_attr(not(target_arch = "wasm32"), test)]
149    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
150    fn assert_send_sync() {
151        fn assert_send_sync<T: Send + Sync>() {}
152        assert_send_sync::<super::PerfwarnInterval>();
153        assert_send_sync::<super::PerfwarnIntervalIf>();
154        assert_send_sync::<super::ProfileInterval>();
155    }
156}
157
158/// A conditional performance warning interval that logs only if execution exceeds a threshold.
159///
160/// Unlike [`PerfwarnInterval`] which always logs, `PerfwarnIntervalIf` only emits a warning
161/// if the measured duration exceeds the specified threshold. This is useful for operations
162/// that are normally fast but occasionally slow down due to external factors.
163///
164/// # Usage
165///
166/// This type is typically not created directly. Instead, use the `perfwarn_begin_if!` macro
167/// which properly sets up the interval with source location information.
168///
169/// # Example
170///
171/// ```rust
172/// logwise::declare_logging_domain!();
173/// # fn main() {
174/// # use std::time::Duration;
175/// # fn perform_database_query() {}
176/// // Create a conditional interval that warns only if execution takes > 100ms
177/// let _interval = logwise::perfwarn_begin_if!(Duration::from_millis(100), "database_query");
178/// perform_database_query();
179/// // If the query took longer than 100ms, a warning is logged when _interval drops
180/// # }
181/// ```
182///
183/// # Behavior
184///
185/// - The interval starts when created
186/// - On drop, if the elapsed time exceeds the threshold:
187///   - The duration is logged at the `PerfWarn` level
188///   - The interval is added to the current task's statistics
189/// - If the elapsed time is below the threshold, no log is emitted
190#[derive(Debug)]
191pub struct PerfwarnIntervalIf {
192    label: &'static str,
193    start: crate::sys::Instant,
194    threshold: crate::sys::Duration,
195    record: crate::log_record::LogRecord,
196}
197
198impl PerfwarnIntervalIf {
199    /// Creates a new conditional performance warning interval.
200    ///
201    /// This method is used internally by the `perfwarn_begin_if!` macro. You should
202    /// generally use the macro rather than calling this method directly, as the macro
203    /// properly captures source location information.
204    ///
205    /// # Arguments
206    ///
207    /// * `label` - A static string identifying this interval in log messages
208    /// * `time` - The start time of the interval
209    /// * `threshold` - The duration threshold; warnings are only logged if exceeded
210    /// * `record` - A pre-populated log record with source location information
211    ///
212    /// # Example
213    ///
214    /// Use the `perfwarn_begin_if!` macro instead:
215    ///
216    /// ```rust
217    /// logwise::declare_logging_domain!();
218    /// # fn main() {
219    /// use std::time::Duration;
220    ///
221    /// let _interval = logwise::perfwarn_begin_if!(
222    ///     Duration::from_millis(100),
223    ///     "database_query"
224    /// );
225    /// // ... perform operation ...
226    /// // Only logs if operation took > 100ms
227    /// # }
228    /// ```
229    #[inline]
230    pub fn new(
231        label: &'static str,
232        time: crate::sys::Instant,
233        threshold: crate::sys::Duration,
234        record: crate::log_record::LogRecord,
235    ) -> Self {
236        Self {
237            label,
238            start: time,
239            threshold,
240            record,
241        }
242    }
243}
244
245impl Drop for PerfwarnIntervalIf {
246    fn drop(&mut self) {
247        let end_time = crate::sys::Instant::now();
248        let duration = end_time.duration_since(self.start);
249        let ctx = Context::current();
250        ctx._add_task_interval_if(self.label, duration, self.threshold);
251
252        if duration > self.threshold {
253            // We need to insert the timestamp before the label (which is already in the record).
254            // The record parts structure is: [Prelude] "PERFWARN: " File ":line:col " [Label]
255            // We want: ... ":line:col " [Timestamp] [Label] " is SLOW: " [Duration]
256
257            let mut insert_idx = 0;
258            let mut found = false;
259            for (i, part) in self.record.parts.iter().enumerate() {
260                if part == "PERFWARN: " {
261                    // Found the marker. The next two parts are File and LineCol.
262                    // So we want to insert at i + 3.
263                    insert_idx = i + 3;
264                    found = true;
265                    break;
266                }
267            }
268
269            if found && insert_idx <= self.record.parts.len() {
270                // Generate timestamp string
271                let mut temp_record = crate::log_record::LogRecord::new(Level::PerfWarn);
272                temp_record.log_time_since(end_time);
273                let timestamp = temp_record.parts.pop().unwrap();
274
275                self.record.parts.insert(insert_idx, timestamp);
276            }
277
278            self.record.log(" is SLOW: ");
279            self.record
280                .log_owned(format!("[interval took {:?}] ", duration));
281
282            let global_loggers = global_loggers();
283            for logger in global_loggers {
284                logger.finish_log_record(self.record.clone());
285            }
286        }
287    }
288}
289
290/// Generates the next unique profile interval ID.
291///
292/// This function is used internally to assign unique IDs to profile intervals,
293/// allowing BEGIN and END messages to be correlated in log output.
294#[inline]
295pub fn next_profile_id() -> u64 {
296    PROFILE_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
297}
298
299/// A profiling interval that logs when created and dropped.
300///
301/// `ProfileInterval` logs a BEGIN message when created and an END message with
302/// elapsed duration when dropped. Each interval has a unique ID to correlate
303/// BEGIN/END pairs, which is especially useful when profiling nested operations.
304///
305/// Unlike [`PerfwarnInterval`], this type:
306/// - Always logs (no threshold)
307/// - Logs both BEGIN and END messages
308/// - Does not contribute to task statistics
309/// - Is intended for temporary profiling that should be removed before committing
310///
311/// # Usage
312///
313/// This type is typically not created directly. Instead, use the `profile_begin!`
314/// macro which properly sets up the interval with source location information.
315///
316/// # Example
317///
318/// ```rust
319/// logwise::declare_logging_domain!();
320/// # fn main() {
321/// # fn perform_operation() {}
322/// // Using profile_begin! for manual interval management
323/// let interval = logwise::profile_begin!("database_query");
324/// perform_operation();
325/// // BEGIN logged when created, END logged when dropped
326/// drop(interval);
327/// # }
328/// ```
329///
330/// # Log Output
331///
332/// The interval produces log messages like:
333/// ```text
334/// PROFILE: BEGIN [id=1] src/main.rs:10:5 [0ns] database_query
335/// PROFILE: END [id=1] [elapsed: 150µs] database_query
336/// ```
337#[derive(Debug)]
338pub struct ProfileInterval {
339    id: u64,
340    label: &'static str,
341    start: crate::sys::Instant,
342}
343
344impl ProfileInterval {
345    /// Creates a new profile interval.
346    ///
347    /// Do not use this manually. Instead use the `profile_begin!` macro which
348    /// properly captures source location information.
349    ///
350    /// # Arguments
351    ///
352    /// * `id` - Unique identifier for correlating BEGIN/END messages
353    /// * `label` - A static string identifying this interval in log messages
354    /// * `time` - The start time of the interval
355    #[inline]
356    pub fn new(id: u64, label: &'static str, time: crate::sys::Instant) -> Self {
357        Self {
358            id,
359            label,
360            start: time,
361        }
362    }
363
364    /// Returns the unique ID of this profile interval.
365    #[inline]
366    pub fn id(&self) -> u64 {
367        self.id
368    }
369}
370
371impl Drop for ProfileInterval {
372    fn drop(&mut self) {
373        let end_time = crate::sys::Instant::now();
374        let duration = end_time.duration_since(self.start);
375
376        let mut record = LogRecord::new(Level::Profile);
377        let ctx = Context::current();
378        ctx._log_prelude(&mut record);
379        record.log_owned(format!("PROFILE: END [id={}] ", self.id));
380        record.log_owned(format!("[elapsed: {:?}] ", duration));
381        record.log(self.label);
382
383        let global_loggers = global_loggers();
384        for logger in global_loggers {
385            logger.finish_log_record(record.clone());
386        }
387    }
388}