evobench_tools/run/
command_log_file.rs

1//! Log files holding benchmarking command output--not the structured
2//! timing data, but stdout and stderr of the benchmarking target of
3//! the target application, plus a header with the serialized
4//! parameters.
5
6use std::{
7    borrow::Cow,
8    path::{Path, PathBuf},
9};
10
11use anyhow::Result;
12use chrono::DateTime;
13
14use crate::{
15    ctx, io_utils::output_capture_log::OutputCaptureLog, io_utils::zstd_file::decompressed_file,
16    run::key::BenchmarkingJobParameters,
17};
18
19/// Returns `(head, rest, rest_lineno)`, where `rest_lineno` is the
20/// 1-based line number where `rest` starts. Returns None if either
21/// head or rest are empty.
22fn split_off_log_file_params(s: &str) -> Option<(&str, &str, usize)> {
23    // Should have added a separator to the files (now it outputs an
24    // empty line, but have to deal with older files, too): scan until
25    // finding the first timestamp, then assume the part before is the
26    // head.
27    let mut line_endings = s.char_indices().filter(|(_, c)| *c == '\n').map(|(i, _)| i);
28    let mut lineno = 1;
29    let mut i = 0; // the start of the next line
30    loop {
31        let rest = &s[i..];
32        if let Some((t, _)) = rest.split_once('\t') {
33            if let Ok(_timestamp) = DateTime::parse_from_rfc3339(t) {
34                if i == 0 {
35                    return None;
36                }
37                let head = &s[0..i - 1];
38                return Some((head, rest, lineno));
39            }
40        }
41        if let Some(i2) = line_endings.next() {
42            lineno += 1;
43            i = i2 + 1;
44        } else {
45            return None;
46        }
47    }
48}
49
50/// A command log file, i.e. stderr and stdout of the benchmarking
51/// target of the target application.
52pub struct CommandLogFile<P: AsRef<Path>> {
53    pub path: P,
54}
55
56// XX should wrap OutputCaptureLog to represent command log files while
57// writing, then translate from *that* to CommandLogFile (which is the
58// non-writing state).
59impl From<OutputCaptureLog> for CommandLogFile<PathBuf> {
60    fn from(value: OutputCaptureLog) -> Self {
61        Self {
62            path: value.into_path(),
63        }
64    }
65}
66
67impl<P: AsRef<Path>> From<P> for CommandLogFile<P> {
68    fn from(path: P) -> Self {
69        Self { path }
70    }
71}
72
73/// The contents of a command log file, split into head and rest if
74/// possible (old versions of those files didn't have a head; probably
75/// should require one at some point).
76#[ouroboros::self_referencing]
77pub struct CommandLog<'l, P: AsRef<Path>> {
78    pub log_file: &'l CommandLogFile<P>,
79    pub contents: String,
80    #[borrows(contents)]
81    #[covariant]
82    /// Only if a head is present; otherwise, borrow `contents` as the
83    /// rest.
84    pub head_and_rest: Option<(&'this str, &'this str, usize)>,
85}
86
87impl<P: AsRef<Path>> CommandLogFile<P> {
88    /// Read the file contents and split it into head and rest if it
89    /// has a detectable head.
90    pub fn command_log<'l>(&'l self) -> Result<CommandLog<'l, P>> {
91        let log_path = self.path.as_ref();
92        let input = decompressed_file(log_path, None)?;
93        let log_contents =
94            std::io::read_to_string(input).map_err(ctx!("reading file {log_path:?}"))?;
95        Ok(CommandLog::new(self, log_contents, |contents| {
96            split_off_log_file_params(contents)
97        }))
98    }
99}
100
101#[derive(thiserror::Error, Debug)]
102pub enum ParseCommandLogError {
103    #[error("parsing command log file {0:?}: {1}")]
104    SerdeError(PathBuf, String),
105    #[error("command log file {0:?} is missing a metadata head")]
106    MissingHead(PathBuf),
107}
108
109impl<'l, P: AsRef<Path>> CommandLog<'l, P> {
110    pub fn path(&self) -> &Path {
111        self.borrow_log_file().path.as_ref()
112    }
113
114    pub fn path_string_lossy<'s>(&'s self) -> Cow<'s, str> {
115        self.path().to_string_lossy()
116    }
117
118    /// Parse the head (not cached)
119    pub fn parse_log_file_params(&self) -> Result<BenchmarkingJobParameters, ParseCommandLogError> {
120        let (head, _rest, _lineno) = self
121            .borrow_head_and_rest()
122            .ok_or_else(|| ParseCommandLogError::MissingHead(self.path().to_owned()))?;
123        serde_yml::from_str(head)
124            .map_err(|e| ParseCommandLogError::SerdeError(self.path().to_owned(), e.to_string()))
125    }
126
127    /// The part of the file contents after the head, together with
128    /// the 1-based line number where it starts. If there was no head
129    /// detected, just give the whole file contents.
130    pub fn log_contents_rest(&self) -> (&str, usize) {
131        if let Some((_, rest, lineno)) = self.borrow_head_and_rest() {
132            (rest, *lineno)
133        } else {
134            (self.borrow_contents(), 1)
135        }
136    }
137}