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}