|
@@ -152,6 +152,96 @@ pub(crate) fn load_report(config: &Config, recipe: &str, file: &str) -> Option<S
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/// Braille spinner frame or progress bar — llama-benchy's rich in-place
|
|
|
|
|
+/// progress redraws.
|
|
|
|
|
+fn is_spinner_frame(line: &str) -> bool {
|
|
|
|
|
+ let t = line.trim_start();
|
|
|
|
|
+ t.starts_with(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']) || t.contains('━')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Progress-redraw identity: the spinner frames differ only by the glyph,
|
|
|
|
|
+/// the elapsed clock and the run/progress counters — same key = same phase
|
|
|
|
|
+/// redrawn in place.
|
|
|
|
|
+fn progress_key(line: &str) -> String {
|
|
|
|
|
+ line.split_whitespace()
|
|
|
|
|
+ .filter(|t| {
|
|
|
|
|
+ // keep only wordish tokens (the test name): bars — the tip glyph
|
|
|
|
|
+ // varies as the bar advances (━╸╺…) — spinner glyphs, @ signs and
|
|
|
|
|
+ // other graphics carry no alphanumerics; counters (0/18), elapsed
|
|
|
|
|
+ // clocks (0:01:48) and the "run N" pair are progress churn.
|
|
|
|
|
+ t.chars().any(|c| c.is_alphanumeric())
|
|
|
|
|
+ && !t.chars().all(|c| c.is_ascii_digit() || c == '/')
|
|
|
|
|
+ && !t.contains(':')
|
|
|
|
|
+ && *t != "run"
|
|
|
|
|
+ })
|
|
|
|
|
+ .collect::<Vec<_>>()
|
|
|
|
|
+ .join(" ")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Make old pty-captured bench logs read like the new ones: drop pty
|
|
|
|
|
+/// control-char artifacts (`^D`/backspace echoes), collapse each spinner
|
|
|
|
|
+/// phase's redraw run down to its last frame, drop consecutive duplicate
|
|
|
|
|
+/// lines. Idempotent on logs that are already clean.
|
|
|
|
|
+pub(crate) fn clean_log(content: &str) -> String {
|
|
|
|
|
+ let lines: Vec<String> = content
|
|
|
|
|
+ .lines()
|
|
|
|
|
+ .map(|l| {
|
|
|
|
|
+ let l: String = l
|
|
|
|
|
+ .chars()
|
|
|
|
|
+ .filter(|c| !matches!(c, '\u{4}' | '\u{8}'))
|
|
|
|
|
+ .collect();
|
|
|
|
|
+ // the pty echoes control chars as caret notation (^D, ^H) —
|
|
|
|
|
+ // printable '^' + @.._ pairs at the line start
|
|
|
|
|
+ let mut l = l.as_str();
|
|
|
|
|
+ while l.starts_with('^')
|
|
|
|
|
+ && l[1..]
|
|
|
|
|
+ .chars()
|
|
|
|
|
+ .next()
|
|
|
|
|
+ .is_some_and(|c| ('@'..='_').contains(&c))
|
|
|
|
|
+ {
|
|
|
|
|
+ l = &l[2..];
|
|
|
|
|
+ }
|
|
|
|
|
+ l.to_string()
|
|
|
|
|
+ })
|
|
|
|
|
+ .collect();
|
|
|
|
|
+ let mut keep = vec![true; lines.len()];
|
|
|
|
|
+ let mut i = 0;
|
|
|
|
|
+ while i < lines.len() {
|
|
|
|
|
+ let prog = is_spinner_frame(&lines[i]);
|
|
|
|
|
+ let key = if prog {
|
|
|
|
|
+ progress_key(&lines[i])
|
|
|
|
|
+ } else {
|
|
|
|
|
+ String::new()
|
|
|
|
|
+ };
|
|
|
|
|
+ let mut j = i + 1;
|
|
|
|
|
+ while j < lines.len() {
|
|
|
|
|
+ let same = if prog {
|
|
|
|
|
+ is_spinner_frame(&lines[j]) && progress_key(&lines[j]) == key
|
|
|
|
|
+ } else {
|
|
|
|
|
+ !lines[i].is_empty() && lines[j] == lines[i]
|
|
|
|
|
+ };
|
|
|
|
|
+ if !same {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ keep[j] = false;
|
|
|
|
|
+ j += 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ if j > i + 1 && prog {
|
|
|
|
|
+ // a redraw run keeps only its final frame (exact dups keep the first)
|
|
|
|
|
+ keep[i] = false;
|
|
|
|
|
+ keep[j - 1] = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ i = j;
|
|
|
|
|
+ }
|
|
|
|
|
+ lines
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .enumerate()
|
|
|
|
|
+ .filter(|(k, _)| keep[*k])
|
|
|
|
|
+ .map(|(_, l)| l.as_str())
|
|
|
|
|
+ .collect::<Vec<_>>()
|
|
|
|
|
+ .join("\n")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/// `GET /r/<recipe>/<file>` — file viewer page: rendered markdown (with a
|
|
/// `GET /r/<recipe>/<file>` — file viewer page: rendered markdown (with a
|
|
|
/// raw toggle), syntax-highlighted TOML/JSON, plain logs.
|
|
/// raw toggle), syntax-highlighted TOML/JSON, plain logs.
|
|
|
const VIEW_TEMPLATE: &str = include_str!("view.html");
|
|
const VIEW_TEMPLATE: &str = include_str!("view.html");
|
|
@@ -172,6 +262,13 @@ async fn report_file(
|
|
|
let clean = String::from_utf8_lossy(&clean)
|
|
let clean = String::from_utf8_lossy(&clean)
|
|
|
.replace("\r\n", "\n")
|
|
.replace("\r\n", "\n")
|
|
|
.replace('\r', "\n");
|
|
.replace('\r', "\n");
|
|
|
|
|
+ // old pty-captured logs carry spinner-redraw soup and pty
|
|
|
|
|
+ // control-char artifacts — render them like the clean ones
|
|
|
|
|
+ let clean = if file.ends_with(".log") {
|
|
|
|
|
+ clean_log(&clean)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ clean
|
|
|
|
|
+ };
|
|
|
let meta = serde_json::json!({ "recipe": recipe, "file": file, "content": clean });
|
|
let meta = serde_json::json!({ "recipe": recipe, "file": file, "content": clean });
|
|
|
let meta = serde_json::to_string(&meta)
|
|
let meta = serde_json::to_string(&meta)
|
|
|
.unwrap_or_default()
|
|
.unwrap_or_default()
|