aleo_std_timer/
lib.rs

1// Copyright (C) 2019-2021 Aleo Systems Inc.
2// This file is part of the aleo-std library.
3
4// The aleo-std library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The aleo-std library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the aleo-std library. If not, see <https://www.gnu.org/licenses/>.
16
17// With credits to PhilipDaniels/logging_timer.
18
19//! This crate implements a straightforward timer to conveniently time code blocks.
20
21#[cfg(test)]
22mod tests;
23
24#[cfg(feature = "timer")]
25use core::{fmt, time::Duration};
26#[cfg(feature = "timer")]
27use std::{
28    cell::RefCell,
29    rc::Rc,
30    sync::atomic::{AtomicBool, AtomicUsize, Ordering},
31    time::Instant,
32};
33
34#[cfg(feature = "timer")]
35use colored::{ColoredString, Colorize};
36
37#[cfg(feature = "timer")]
38pub static NUM_INDENT: AtomicUsize = AtomicUsize::new(0);
39#[cfg(feature = "timer")]
40pub const PAD_CHAR: &str = " ";
41
42/// When this struct is dropped, it logs a message stating its name and how long
43/// the execution time was. Can be used to time functions or other critical areas.
44#[cfg(feature = "timer")]
45pub struct Timer<'name> {
46    /// The instant, in UTC, that the timer was instantiated.
47    start_time: Instant,
48    /// The last lap time, in UTC, that the timer was called.
49    last_lap_time: Rc<RefCell<Instant>>,
50    /// Set by the module_path!() macro to the module where the timer is instantiated.
51    #[allow(dead_code)]
52    module_path: &'static str,
53    /// Set by the file!() macro to the name of the file where the timer is instantiated.
54    file: &'static str,
55    /// Set by the line!() macro to the line number where the timer is instantiated.
56    line: u32,
57    /// The name of the timer. Used in messages to identify it.
58    name: &'name str,
59    /// The level of indentation for this timer context.
60    indent: usize,
61    /// A flag used to suppress printing of the 'Finish' message in the drop() function
62    /// It is set by the finish method.
63    finished: AtomicBool,
64    /// Any extra information to be logged along with the name. Unfortunately, due
65    /// to the lifetimes associated with a `format_args!` invocation, this currently allocates
66    /// if you use it.
67    extra_info: Option<String>,
68}
69
70#[cfg(not(feature = "timer"))]
71#[derive(Default)]
72pub struct Timer<'name> {
73    _marker: std::marker::PhantomData<&'name ()>,
74}
75
76impl<'name> Timer<'name> {
77    /// Constructs a new `Timer` that prints a 'Start' and a 'Finish' message.
78    /// This method is not usually called directly, use the `timer!` macro instead.
79    #[cfg(feature = "timer")]
80    pub fn new(
81        file: &'static str,
82        module_path: &'static str,
83        line: u32,
84        name: &'name str,
85        extra_info: Option<String>,
86    ) -> Option<Self> {
87        let start_time = Instant::now();
88        let timer = Timer {
89            start_time,
90            last_lap_time: Rc::new(RefCell::new(start_time)),
91            module_path,
92            file,
93            line,
94            name,
95            indent: NUM_INDENT.fetch_add(0, Ordering::Relaxed),
96            finished: AtomicBool::new(false),
97            extra_info,
98        };
99        // Print the start message.
100        timer.print(TimerState::Start, None);
101        // Increment the indentation by 1.
102        NUM_INDENT.fetch_add(1, Ordering::Relaxed);
103        Some(timer)
104    }
105
106    #[cfg(not(feature = "timer"))]
107    pub fn noop(&self) {}
108
109    /// Returns how long the timer has been running for.
110    #[cfg(feature = "timer")]
111    pub fn elapsed(&self, elapsed: Duration) -> String {
112        let secs = elapsed.as_secs();
113        let millis = elapsed.subsec_millis();
114        let micros = elapsed.subsec_micros() % 1000;
115        let nanos = elapsed.subsec_nanos() % 1000;
116        if secs != 0 {
117            format!("{}.{:0>3}s", secs, millis)
118        } else if millis > 0 {
119            format!("{}.{:0>3}ms", millis, micros)
120        } else if micros > 0 {
121            format!("{}.{:0>3}µs", micros, nanos)
122        } else {
123            format!("{}ns", elapsed.subsec_nanos())
124        }
125    }
126
127    /// Returns how long the timer has been running for.
128    #[cfg(feature = "timer")]
129    pub fn elapsed_colored(&self, elapsed: Duration) -> ColoredString {
130        let secs = elapsed.as_secs();
131        let millis = elapsed.subsec_millis();
132        let micros = elapsed.subsec_micros() % 1000;
133        let nanos = elapsed.subsec_nanos() % 1000;
134        if secs != 0 {
135            format!("{}.{:0>3}s", secs, millis).magenta().bold()
136        } else if millis > 0 {
137            format!("{}.{:0>3}ms", millis, micros).yellow().bold()
138        } else if micros > 0 {
139            format!("{}.{:0>3}µs", micros, nanos).cyan().bold()
140        } else {
141            format!("{}ns", elapsed.subsec_nanos()).green().bold()
142        }
143    }
144
145    /// Outputs a log message with a target of 'Lap' showing the current elapsed time, but does not
146    /// stop the timer. This method can be called multiple times.
147    /// The message can include further information via a `format_args!` approach.
148    /// This method is usually not called directly, it is easier to use the `lap!` macro.
149    #[cfg(feature = "timer")]
150    pub fn lap(&self, args: Option<fmt::Arguments>) {
151        self.print(TimerState::Lap, args);
152    }
153
154    /// Outputs a log message with a target of 'Finish' and suppresses the normal message
155    /// that is output when the timer is dropped. The message can include further `format_args!`
156    /// information. This method is normally called using the `finish!` macro. Calling
157    /// `finish()` again will have no effect.
158    #[cfg(feature = "timer")]
159    pub fn finish(&self, args: Option<fmt::Arguments>) {
160        if !self.finished.load(Ordering::SeqCst) {
161            // Decrement the indentation by 1.
162            NUM_INDENT.fetch_sub(1, Ordering::Relaxed);
163            self.finished.store(true, Ordering::SeqCst);
164            self.print(TimerState::Finish, args);
165        }
166    }
167
168    #[cfg(feature = "timer")]
169    fn print(&self, state: TimerState, args: Option<fmt::Arguments>) {
170        println!("{}", self.format(state, args));
171    }
172
173    #[cfg(feature = "timer")]
174    fn format(&self, status: TimerState, args: Option<fmt::Arguments>) -> String {
175        // Construct the user message.
176        let user_message = match (self.extra_info.as_ref(), args) {
177            (Some(info), Some(args)) => format!("{}, {}, {}", self.name, info, args),
178            (Some(info), None) => format!("{}, {}", self.name, info),
179            (None, Some(args)) => format!("{}, {}", self.name, args),
180            (None, None) => format!("{}", self.name),
181        };
182
183        // Construct the main message.
184        match status {
185            TimerState::Start => {
186                // Compute the indentation.
187                let indentation_amount = self.indent * 4;
188                let mut indentation = String::new();
189                for _ in 0..indentation_amount {
190                    indentation.push_str(&PAD_CHAR);
191                }
192
193                let message = format!("{} ({})", Self::status(status, self.indent), user_message);
194                // let metadata = format!(" [{} {} L{}]", self.module_path, self.file, self.line).bold();
195                let metadata = format!(" [{} L{}]", self.file, self.line).bold();
196
197                format!(" {indentation}{:<30} {:.>55}", message, metadata)
198            }
199            TimerState::Lap => {
200                // Compute the indentation.
201                let indentation_amount = (self.indent + 1) * 4;
202                let mut indentation = String::new();
203                for _ in 0..indentation_amount {
204                    indentation.push_str(&PAD_CHAR);
205                }
206
207                let message = format!("{} ({})", Self::status(status, self.indent + 1), user_message);
208                let elapsed = self.elapsed_colored(self.last_lap_time.borrow().elapsed());
209
210                // Update the last lap time.
211                *(*self.last_lap_time).borrow_mut() = Instant::now();
212
213                format!(" {indentation}{:<30} {:.>55}", message, elapsed)
214            }
215            TimerState::Finish => {
216                // Compute the indentation.
217                let indentation_amount = self.indent * 4;
218                let mut indentation = String::new();
219                for _ in 0..indentation_amount {
220                    indentation.push_str(&PAD_CHAR);
221                }
222
223                let message = format!("{} ({})", Self::status(status, self.indent), user_message);
224                let elapsed = self.elapsed(self.start_time.elapsed());
225
226                format!(" {indentation}{:<50} {:.>25}", message, elapsed)
227            }
228        }
229    }
230
231    /// Returns the state of the timer, with coloring.
232    #[cfg(feature = "timer")]
233    fn status(status: TimerState, indent: usize) -> ColoredString {
234        let status = match status {
235            TimerState::Start => "Start",
236            TimerState::Lap => "Lap",
237            TimerState::Finish => "Finish",
238        };
239
240        match indent % 5 {
241            0 => Colorize::green(status).bold(),
242            1 => Colorize::cyan(status).bold(),
243            2 => Colorize::yellow(status).bold(),
244            3 => Colorize::magenta(status).bold(),
245            4 => Colorize::red(status).bold(),
246            _ => Colorize::white(status).bold(),
247        }
248    }
249}
250
251#[cfg(feature = "timer")]
252impl<'a> Drop for Timer<'a> {
253    /// Drops the timer, outputting a log message with a target of `Finish`
254    /// if the `finish` method has not yet been called.
255    fn drop(&mut self) {
256        self.finish(None);
257    }
258}
259
260#[cfg(feature = "timer")]
261#[derive(Debug, Copy, Clone)]
262enum TimerState {
263    Start,
264    Lap,
265    Finish,
266}
267
268/// Initializes a timer that logs a start and finish message.
269///
270/// # Examples
271/// Note that when specifying the log level you must use a semi-colon as a
272/// separator, this is to ensure disambiguous parsing of the macro arguments.
273///
274/// ```
275/// use aleo_std_timer::timer;
276///
277/// let _tmr1 = timer!("FIND_FILES");
278/// let _tmr2 = timer!("FIND_FILES", "Found {} files", 42);
279/// ```
280#[cfg(feature = "timer")]
281#[macro_export]
282macro_rules! timer {
283    ($name:expr) => {
284        {
285            $crate::Timer::new(
286                file!(),
287                module_path!(),
288                line!(),
289                $name,
290                None,
291                )
292        }
293    };
294
295    ($name:expr, $format:tt) => {
296        {
297            $crate::Timer::new(
298                file!(),
299                module_path!(),
300                line!(),
301                $name,
302                Some(format!($format)),
303                )
304        }
305    };
306
307    ($name:expr, $format:tt, $($arg:expr),*) => {
308        {
309            $crate::Timer::new(
310                file!(),
311                module_path!(),
312                line!(),
313                $name,
314                Some(format!($format, $($arg), *)),
315                )
316        }
317    };
318}
319
320/// Makes an existing timer output an 'lap' message.
321/// Can be called multiple times.
322#[cfg(feature = "timer")]
323#[macro_export]
324macro_rules! lap {
325    ($timer:expr) => ({
326        if let Some(ref timer) = $timer {
327            timer.lap(None);
328        }
329    });
330
331    ($timer:expr, $format:tt) => ({
332        if let Some(ref timer) = $timer {
333            timer.lap(Some(format_args!($format)))
334        }
335    });
336
337    ($timer:expr, $format:tt, $($arg:expr),*) => ({
338        if let Some(ref timer) = $timer {
339            timer.lap(Some(format_args!($format, $($arg), *)))
340        }
341    })
342}
343
344/// Makes an existing timer output a 'finished' message and suppresses
345/// the normal drop message.
346/// Only the first call has any effect, subsequent calls will be ignored.
347#[cfg(feature = "timer")]
348#[macro_export]
349macro_rules! finish {
350    ($timer:expr) => ({
351        if let Some(ref timer) = $timer {
352            timer.finish(None)
353        }
354    });
355
356    ($timer:expr, $format:tt) => ({
357        if let Some(ref timer) = $timer {
358            timer.finish(Some(format_args!($format)))
359        }
360    });
361
362    ($timer:expr, $format:tt, $($arg:expr),*) => ({
363        if let Some(ref timer) = $timer {
364            timer.finish(Some(format_args!($format, $($arg), *)))
365        }
366    })
367}
368
369#[cfg(not(feature = "timer"))]
370#[macro_export]
371macro_rules! timer {
372    ($name:expr) => {
373        $crate::Timer::default()
374    };
375
376    ($name:expr, $format:tt) => {
377        $crate::Timer::default()
378    };
379
380    ($name:expr, $format:tt, $($arg:expr),*) => {
381        $crate::Timer::default()
382    };
383}
384
385#[cfg(not(feature = "timer"))]
386#[macro_export]
387macro_rules! lap {
388    ($timer:expr) => {
389        $timer.noop()
390    };
391
392    ($timer:expr, $format:tt) => {
393        $timer.noop()
394    };
395
396    ($timer:expr, $format:tt, $($arg:expr),*) => {
397        $timer.noop()
398    };
399}
400
401#[cfg(not(feature = "timer"))]
402#[macro_export]
403macro_rules! finish {
404    ($timer:expr) => {
405        $timer.noop()
406    };
407
408    ($timer:expr, $format:tt) => {
409        $timer.noop()
410    };
411
412    ($timer:expr, $format:tt, $($arg:expr),*) => {
413        $timer.noop()
414    };
415}