rtss/
lib.rs

1//! rtss - Relative TimeStamps for Stuff.
2//!
3//! An `io::Write` implementation which prefixes each line with a timestamp since
4//! a start time, and the duration since the previous line, if any.
5//!
6//! Also a couple of utility functions for formatting `Duration`, and copying from
7//! one IO to another.
8
9use std::io::{self, Cursor};
10use std::time::{Duration, Instant};
11
12use memchr::memchr;
13
14pub trait DurationExt {
15    /// Write a `Duration` to a formatted form for human consumption.
16    fn write_human<W: io::Write>(&self, out: &mut W) -> io::Result<()>;
17
18    /// Write a `Duration` to a formatted form sortable lexographically,
19    /// like "15:04:05.421224"
20    fn write_sortable<W: io::Write>(&self, out: &mut W) -> io::Result<()>;
21
22    /// Return the results of `write_human()` as a new `String`
23    fn human_string(&self) -> String {
24        let mut v = Cursor::new(Vec::with_capacity(16));
25        self.write_human(&mut v).unwrap();
26        String::from_utf8(v.into_inner()).unwrap()
27    }
28
29    /// Return the results of `write_sortable()` as a new `String`
30    fn sortable_string(&self) -> String {
31        let mut v = Cursor::new(Vec::with_capacity(16));
32        self.write_sortable(&mut v).unwrap();
33        String::from_utf8(v.into_inner()).unwrap()
34    }
35}
36
37impl DurationExt for Duration {
38    fn write_human<W: io::Write>(&self, out: &mut W) -> io::Result<()> {
39        let mut ts = self.as_secs();
40        let ns = self.subsec_nanos();
41
42        if ts > 0 {
43            let mut cs = (f64::from(ns) / 10_000_000_f64).round() as u8;
44            if cs == 100 {
45                // round up to the nearest centisecond
46                ts += 1;
47                cs = 0;
48            }
49
50            let mut s = ts;
51
52            if ts >= 86400 {
53                write!(out, "{}d", s / 86400)?;
54                s %= 86400;
55            }
56
57            if ts >= 3600 {
58                write!(out, "{}h", s / 3600)?;
59                s %= 3600;
60            }
61
62            if ts >= 60 {
63                write!(out, "{}m", s / 60)?;
64                s %= 60;
65            }
66
67            write!(out, "{}.{:02}s", s, cs)?;
68        } else if ns > 100_000 {
69            write!(out, "{:.1}ms", f64::from(ns) / 1_000_000_f64)?;
70        } else if ns > 100 {
71            write!(out, "{:.1}μs", f64::from(ns) / 1_000_f64)?;
72        }
73        Ok(())
74    }
75
76    fn write_sortable<W: io::Write>(&self, out: &mut W) -> io::Result<()> {
77        let ts = self.as_secs();
78        let us = self.subsec_micros();
79
80        write!(
81            out,
82            "{:02}:{:02}:{:02}.{:06}",
83            ts / 3600,
84            (ts % 3600) / 60,
85            ts % 60,
86            us
87        )
88    }
89}
90
91pub type DurationFormatter = fn(&Duration) -> String;
92
93/// A writer that prefixes all lines with relative timestamps.
94pub struct RtssWriter<W> {
95    inner: W,
96    formatter: DurationFormatter,
97    separator: char,
98    start: Instant,
99    last: Instant,
100    at_eol: bool,
101}
102
103impl<W: io::Write> RtssWriter<W> {
104    /// Create a new `RtssWriter`, with a given start time, `Duration` formatter,
105    /// and delimiter separating the prefix and content.
106    ///
107    /// ```
108    /// use std::io::{self, Write};
109    /// use std::time::{Duration, Instant};
110    ///
111    /// extern crate rtss;
112    /// use rtss::{RtssWriter, DurationExt};
113    ///
114    /// fn main() -> io::Result<()> {
115    ///     let mut writer = RtssWriter::new(io::stdout(), Duration::human_string, '|', Instant::now());
116    ///     writer.write(b"Hello!\n")?;
117    ///     writer.write(b"World!\n")?;
118    ///     Ok(())
119    /// }
120    ///
121    /// // Expected output:
122    /// //   0.2μs    0.2μs | Hello!
123    /// //  84.7μs   84.6μs | World!
124    /// ```
125    pub fn new(inner: W, formatter: DurationFormatter, separator: char, now: Instant) -> Self {
126        Self {
127            inner,
128            formatter,
129            separator,
130            start: now,
131            last: now,
132            at_eol: true,
133        }
134    }
135}
136
137impl<W: io::Write> io::Write for RtssWriter<W> {
138    /// Writes the contents of `buf` to the underlying writer, with time annotations
139    /// for any new lines.
140    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
141        let now = Instant::now();
142        let start_duration = (self.formatter)(&now.duration_since(self.start));
143        let line_duration = (self.formatter)(&now.duration_since(self.last));
144
145        let pfx_start = format!(
146            "{:>8} {:>8} {} ",
147            start_duration, line_duration, self.separator
148        );
149        let pfx_rest = format!(
150            "{:>8} {:>8} {} ",
151            start_duration,
152            (self.formatter)(&Duration::new(0, 0)),
153            self.separator
154        );
155
156        let mut pos: usize = 0;
157        let mut saw_eol = false;
158        let mut first = true;
159
160        let n = buf.len();
161
162        while pos < n {
163            if self.at_eol {
164                if first {
165                    self.inner.write_all(pfx_start.as_bytes())?;
166                    first = false;
167                } else {
168                    self.inner.write_all(pfx_rest.as_bytes())?;
169                }
170            }
171
172            if let Some(newline) = memchr(b'\n', &buf[pos..n]) {
173                saw_eol = true;
174                self.at_eol = true;
175                self.inner.write_all(&buf[pos..=pos + newline])?;
176                pos += newline + 1;
177            } else {
178                self.at_eol = false;
179                self.inner.write_all(&buf[pos..n])?;
180                break;
181            }
182        }
183
184        self.inner.flush()?;
185
186        if saw_eol {
187            self.last = now;
188        }
189
190        Ok(n)
191    }
192
193    fn flush(&mut self) -> io::Result<()> {
194        self.inner.flush()
195    }
196}
197
198/// Copy each line from `input` to `output`, prepending the output line with
199/// elapsed time since `start` and since the previous line, separated by `separator`
200/// until EOF or IO error.
201///
202/// Returns the number of bytes read from `input` on success.
203pub fn line_timing_copy<R: io::Read, W: io::Write>(
204    mut input: &mut R,
205    output: &mut W,
206    formatter: DurationFormatter,
207    separator: char,
208    start: Instant,
209) -> io::Result<u64> {
210    let output = io::BufWriter::new(output);
211    let mut output = RtssWriter::new(output, formatter, separator, start);
212
213    io::copy(&mut input, &mut output)
214}