atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git
xtask/src/log/timer.rsRust266 lines7.9 KB
1
use std::{
2
    fmt, fs,
3
    io::{self, BufRead, Write},
4
    path,
5
    str::FromStr,
6
    time::Duration,
7
};
8
9
use chrono::{DateTime, FixedOffset};
10
use regex::RegexBuilder;
11
12
use crate::Result;
13
14
const OUTPUT_TEMPLATE: &str = include_str!("timer.template.html");
15
16
pub(super) fn run(log_path: path::PathBuf, output_path: path::PathBuf) -> Result<()> {
17
    let find_timer = RegexBuilder::new(
18
        r#"
19
            # Let's start.
20
            ^
21
22
            # Datetime of the log line.
23
            (?<datetime>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)
24
25
            # Log level.
26
            \s+(?<level>\S+)
27
28
            # Target.
29
            \s(?<target>matrix_[\w_]+(::[\w_]+)*):
30
31
            # The timer message.
32
            \s(?<message>(Timer\s)?_.*?_)
33
            \sfinished\sin
34
            # The timer duration.
35
            \s(?<duration>\d(\.\d+)?.*s)
36
37
            # The source file and line.
38
            \s\|\scrates/
39
            (?<source_file>[^:]+)
40
            :(?<source_line>\d+)
41
        "#,
42
    )
43
    .ignore_whitespace(true)
44
    .build()
45
    .expect("Failed to build the `find_timer` regex");
46
47
    let log_file = fs::File::open(&log_path)?;
48
    let mut output_file = io::BufWriter::new(fs::File::create(&output_path)?);
49
50
    let reader = io::BufReader::new(log_file);
51
    let mut number_of_analysed_lines = 0;
52
    let mut number_of_matched_lines = 0;
53
54
    let mut timers = Vec::<Timer>::new();
55
56
    for line in reader.lines().enumerate().map(|(nth, line)| {
57
        line.unwrap_or_else(|error| {
58
            panic!("Failed to read line #{nth}\n{error}");
59
        })
60
    }) {
61
        number_of_analysed_lines += 1;
62
63
        if let Some(captures) = find_timer.captures(&line) {
64
            number_of_matched_lines += 1;
65
66
            let date_time = DateTime::parse_from_rfc3339(
67
                captures.name("datetime").expect("Failed to capture `datetime`").as_str(),
68
            )
69
            .expect("Failed to parse `datetime`");
70
            let level = captures
71
                .name("level")
72
                .expect("Failed to capture `level`")
73
                .as_str()
74
                .parse()
75
                .expect("Failed to parse the `level`");
76
            let target = captures.name("target").expect("Failed to capture `target`").as_str();
77
            let message = captures.name("message").expect("Failed to capture `message`").as_str();
78
            let duration_as_str =
79
                captures.name("duration").expect("Failed to capture `duration`").as_str();
80
            let duration = {
81
                let duration = duration_as_str;
82
83
                // According to
84
                // https://github.com/rust-lang/rust/blob/0028f344ce9f64766259577c998a1959ca1f6a0b/library/core/src/time.rs#L1488-L15
85
                // 10, there is 4 possible suffixes. Let's get through them.
86
87
                let pivot = duration.floor_char_boundary(duration.len().saturating_sub(2));
88
89
                Duration::from_nanos(match &duration[pivot..] {
90
                    // nanoseconds
91
                    "ns" => {
92
                        let duration: f64 = duration[..pivot].parse().unwrap();
93
94
                        duration.abs().round() as u64
95
                    }
96
97
                    "µs" => {
98
                        let duration: f64 = duration[..pivot].parse().unwrap();
99
                        (duration * 1_000.0).abs().round() as u64
100
                    }
101
102
                    "ms" => {
103
                        let duration: f64 = duration[..pivot].parse().unwrap();
104
105
                        (duration * 1_000_000.0).abs().round() as u64
106
                    }
107
108
                    // seconds!
109
                    _ => {
110
                        let duration: f64 = duration[..pivot.saturating_add(1)].parse().unwrap();
111
112
                        (duration * 1_000_000_000.0).abs().round() as u64
113
                    }
114
                })
115
            };
116
            let source_file =
117
                captures.name("source_file").expect("Failed to capture `source_file`").as_str();
118
            let source_line = captures
119
                .name("source_line")
120
                .expect("Failed to capture `source_line`")
121
                .as_str()
122
                .parse()
123
                .expect("Failed to parse the `source_line`");
124
125
            // Ensure we have parsed the duration correctly (in case Rust stdlib changes
126
            // something).
127
            assert_eq!(
128
                duration_as_str,
129
                format!("{duration:?}"),
130
                "Failed to parse the `duration` as expected"
131
            );
132
133
            timers.push(Timer {
134
                date_time,
135
                level,
136
                target: target.to_owned(),
137
                message: message[1..message.len() - 1].to_owned(),
138
                duration,
139
                source_file: source_file.to_owned(),
140
                source_line,
141
            });
142
        }
143
    }
144
145
    timers.sort_by(|a, b| a.duration.cmp(&b.duration).reverse());
146
147
    fn render(timers: Vec<Timer>, buffer: &mut io::BufWriter<fs::File>) {
148
        for Timer { duration, level, message, target, source_file, source_line, date_time } in
149
            timers
150
        {
151
            writeln!(
152
                buffer,
153
                "<tr>\n\
154
                <td>\n\
155
                  <div class=\"span\" style=\"--duration: {duration}\"><span>{duration_label:?}</span></div>\n\
156
                </td>\n\
157
                <td>{message}</td>\n\
158
                <td><span class=\"{level_class}\">{level}</span></td>\n\
159
                <td><code>{target}</code></td>\n\
160
                <td><code>{source_file}:{source_line}</code></td>\n\
161
                <td><time datetime=\"{date_time}\">{time}</time></td>\n\
162
                </tr>\n",
163
                level_class = level.css(),
164
                duration = duration.as_nanos(),
165
                duration_label = duration,
166
                date_time = date_time.format("%+"),
167
                time = date_time.format("%H:%M:%S%.3f"),
168
            )
169
            .unwrap();
170
        }
171
    }
172
173
    let output = OUTPUT_TEMPLATE.replace("{log_file}", &log_path.to_string_lossy()).replace(
174
        "{longest-duration}",
175
        &timers.first().map(|timer| timer.duration.as_nanos()).unwrap_or(0).to_string(),
176
    );
177
178
    output_file.write_all(output.as_bytes()).expect("Failed to write the output");
179
180
    render(timers, &mut output_file);
181
182
    output_file
183
        .write_all(
184
            "  </tbody>
185
</table>
186
187
</main>
188
189
</body>
190
</html>"
191
                .as_bytes(),
192
        )
193
        .expect("Failed to write the output");
194
195
    println!(
196
        "\nNumber of analysed log lines: {number_of_analysed_lines}\n\
197
        Number of matched lines: {number_of_matched_lines}\n\
198
        Output file: {output_path:?}\n\
199
        Done!"
200
    );
201
202
    Ok(())
203
}
204
205
#[derive(Debug)]
206
struct Timer {
207
    date_time: DateTime<FixedOffset>,
208
    level: Level,
209
    target: String,
210
    message: String,
211
    duration: Duration,
212
    source_file: String,
213
    source_line: usize,
214
}
215
216
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
217
enum Level {
218
    Error,
219
    Warn,
220
    Info,
221
    Debug,
222
    Trace,
223
}
224
225
impl Level {
226
    fn css(&self) -> &str {
227
        match self {
228
            Self::Error => "error",
229
            Self::Warn => "warn",
230
            Self::Info => "info",
231
            Self::Debug => "debug",
232
            Self::Trace => "trace",
233
        }
234
    }
235
}
236
237
impl FromStr for Level {
238
    type Err = ();
239
240
    fn from_str(str: &str) -> Result<Self, Self::Err> {
241
        Ok(match str {
242
            "ERROR" => Self::Error,
243
            "WARN" => Self::Warn,
244
            "INFO" => Self::Info,
245
            "DEBUG" => Self::Debug,
246
            "TRACE" => Self::Trace,
247
            _ => return Err(()),
248
        })
249
    }
250
}
251
252
impl fmt::Display for Level {
253
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
254
        write!(
255
            formatter,
256
            "{}",
257
            match self {
258
                Self::Error => "ERROR",
259
                Self::Warn => "WARNING",
260
                Self::Info => "INFO",
261
                Self::Debug => "DEBUG",
262
                Self::Trace => "TRACE",
263
            }
264
        )
265
    }
266
}