Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions crates/gk/src/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//!
//! The status section of `README.md` is rewritten too, between its markers (see [`crate::status`]).
//!
//! The heat maps go to `reports/matrix-<platform>.md`: a table per configuration, a row per kernel that has a cell, a column per GCC that targets the platform, and one letter per cell. The per-GCC and per-kernel pages wait for `gk explain`.
//! The heat maps go to `reports/matrix-<platform>.md`: a table per configuration, a row per kernel that has a cell, a column per GCC that targets the platform, and one colored square per cell. The per-GCC and per-kernel pages wait for `gk explain`.

use crate::cell::CellRecord;
use crate::store;
Expand Down Expand Up @@ -165,17 +165,25 @@ pub fn matrix(ungraded: bool) -> Result<Matrix, String> {
})
}

/// The heat map letter of a cell: `W`, `R`, `B`, `F`, or `·` for n/a.
pub(crate) fn letter(verdict: &str) -> char {
/// The heat map square of a verdict: green works, yellow runs, orange builds, red fails, or `·` for n/a.
pub(crate) fn letter(verdict: &str) -> &'static str {
match verdict {
"works" => 'W',
"runs" => 'R',
"builds" => 'B',
"fails" => 'F',
_ => '·',
"works" => "🟩",
"runs" => "🟨",
"builds" => "🟧",
"fails" => "🟥",
_ => "·",
}
}

/// The square of a cell in a heat map, with ⚠️ after it when its boots disagreed.
pub(crate) fn square(e: &Entry) -> String {
format!("{}{}", letter(&e.verdict), if e.flaky { "⚠️" } else { "" })
}

/// The legend under every heat map.
pub(crate) const LEGEND: &str = "🟩 works, 🟨 runs, 🟧 builds, 🟥 fails, · n/a, and blank where the cell has not run yet. ⚠️ marks a flaky cell, whose boots disagreed and which keeps the lowest rung.";

/// The heat map of one platform, or `None` when it has no cells.
#[must_use]
pub fn heat_map(repo: &Repo, m: &Matrix, platform: &str) -> Option<String> {
Expand All @@ -197,7 +205,7 @@ pub fn heat_map(repo: &Repo, m: &Matrix, platform: &str) -> Option<String> {
configs.dedup();

let mut out = format!(
"# {platform}\n\nOne letter per cell: W works, R runs, B builds, F fails, · n/a, and blank where the cell has not run. A cell marked with `*` was flaky, its boots disagreed and it keeps the lowest. Written by `gk publish` from `matrix/matrix.json`.\n"
"# {platform}\n\nOne square per cell: {LEGEND} Written by `gk publish` from `matrix/matrix.json`.\n"
);
for config in configs {
let mut kernels: Vec<Version> = cells
Expand All @@ -208,9 +216,7 @@ pub fn heat_map(repo: &Repo, m: &Matrix, platform: &str) -> Option<String> {
kernels.sort();
kernels.dedup();
let _ = write!(out, "\n## {config}\n\n");
table(&mut out, &cells, config, &kernels, &gccs, |e| {
format!("{}{}", letter(&e.verdict), if e.flaky { "*" } else { "" })
});
table(&mut out, &cells, config, &kernels, &gccs, square);
if cells
.iter()
.any(|e| e.config == config && e.warnings.is_some_and(|n| n > 0))
Expand Down Expand Up @@ -325,7 +331,7 @@ mod tests {
}

#[test]
fn a_heat_map_has_a_row_per_kernel_and_a_letter_per_cell() {
fn a_heat_map_has_a_row_per_kernel_and_a_square_per_cell() {
let repo = Repo::load(Path::new("../..")).unwrap();
let mut newer = cell("7.2.8", "gcc-16.2.0", "runs", false);
newer.started = 5;
Expand All @@ -346,9 +352,9 @@ mod tests {
.collect();
assert_eq!(rows.len(), 4);
assert!(rows[0].starts_with("| 6.18 |"));
assert!(rows[0].ends_with(" W* |"));
assert!(rows[1].starts_with("| 7.2.8 | F |"));
assert!(rows[1].ends_with(" R |"));
assert!(rows[0].ends_with(" 🟩⚠️ |"));
assert!(rows[1].starts_with("| 7.2.8 | 🟥 |"));
assert!(rows[1].ends_with(" 🟨 |"));
assert!(rows[3].starts_with("| 7.2.8 | |"));
assert!(rows[3].ends_with(" 12 |"));
assert!(heat_map(&repo, &m, "arm64").is_none());
Expand Down
122 changes: 94 additions & 28 deletions crates/gk/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! The section sits between two marker comments and is replaced whole each time, so the rest of the README stays hand written. It has progress bars for the stripes G1 runs, a chart of verdicts and one of build times per GCC column, the newest cells, and a heat map per platform and configuration with a row for every Current kernel, so the empty squares show what is still to run.

use crate::publish::{Entry, Matrix, letter, table};
use crate::publish::{Entry, LEGEND, Matrix, square, table};
use gk_model::Version;
use gk_model::repo::Repo;
use std::fmt::Write as _;
Expand All @@ -21,14 +21,29 @@ type Crossing = (Version, String, String, &'static str);
/// Crossings grouped under a label, one progress line each.
type Rows = Vec<(String, Vec<Crossing>)>;

/// A bar `width` characters long, `#` for the done part and `.` for the rest.
/// The blocks for one to seven eighths of a character.
const EIGHTHS: [char; 7] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉'];

/// `eighths` eighths of a character drawn in blocks, padded with `pad` to `width` characters.
fn blocks(eighths: usize, width: usize, pad: char) -> String {
let eighths = eighths.min(width * 8);
let mut out = "█".repeat(eighths / 8);
if !eighths.is_multiple_of(8) {
out.push(EIGHTHS[eighths % 8 - 1]);
}
let used = eighths.div_ceil(8);
out.extend(std::iter::repeat_n(pad, width - used));
out
}

/// A progress bar `width` characters long, solid for the done part and shaded for the rest. Anything done shows at least an eighth.
fn bar(done: usize, total: usize, width: usize) -> String {
let filled = if total == 0 {
let eighths = if total == 0 {
0
} else {
(done * width).div_ceil(total).min(width)
(done * width * 8).div_ceil(total)
};
format!("{}{}", "#".repeat(filled), ".".repeat(width - filled))
blocks(eighths, width, '░')
}

/// `1 cell`, `2 cells`.
Expand Down Expand Up @@ -86,7 +101,7 @@ fn progress(out: &mut String, m: &Matrix, rows: &[(String, Vec<Crossing>)]) {
total += t;
let _ = writeln!(
lines,
"{label:<24} [{}] {d:>4}/{t:<4} {:>3}%",
"{label:<24} {} {d:>4}/{t:<4} {:>3}%",
bar(d, t, 30),
percent(d, t)
);
Expand All @@ -95,7 +110,7 @@ fn progress(out: &mut String, m: &Matrix, rows: &[(String, Vec<Crossing>)]) {
out.push_str(&lines);
let _ = writeln!(
out,
"{:<24} [{}] {done:>4}/{total:<4} {:>3}%",
"{:<24} {} {done:>4}/{total:<4} {:>3}%",
"total",
bar(done, total, 30),
percent(done, total)
Expand Down Expand Up @@ -164,24 +179,53 @@ fn chart(out: &mut String, rows: &[(String, f64, String)]) {
let max = rows.iter().map(|r| r.1).fold(0.0_f64, f64::max);
out.push_str("```text\n");
for (label, value, note) in rows {
// The chart is 40 characters wide at most; a non-zero value always gets one mark.
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
// The chart is 40 characters wide at most, drawn in eighths; a non-zero value always gets one.
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let n = if max > 0.0 {
((value / max) * 40.0)
((value / max) * 320.0)
.round()
.max(if *value > 0.0 { 1.0 } else { 0.0 }) as usize
} else {
0
};
let _ = writeln!(out, "{label:<12} {:<40} {note}", "#".repeat(n));
let _ = writeln!(out, "{label:<12} {} {note}", blocks(n, 40, ' '));
}
out.push_str("```\n");
}

/// A table of the newest cells per GCC column: how many of each verdict, and a bar for the share that works.
fn by_gcc(out: &mut String, gccs: &[(&str, Version)], latest: &[&Entry]) {
let rows: Vec<(&Version, Vec<&&Entry>)> = gccs
.iter()
.map(|(id, v)| {
(
v,
latest.iter().filter(|e| e.gcc == *id).collect::<Vec<_>>(),
)
})
.filter(|(_, cells)| !cells.is_empty())
.collect();
if rows.is_empty() {
return;
}
out.push_str("\n### By GCC column\n\nThe newest cell at each crossing again, over every kernel, platform and configuration.\n\n| GCC | Cells | 🟩 | 🟨 | 🟧 | 🟥 | ⚠️ | Works |\n|---|--:|--:|--:|--:|--:|--:|---|\n");
for (v, cells) in rows {
let n = |verdict: &str| cells.iter().filter(|e| e.verdict == verdict).count();
let works = n("works");
let _ = writeln!(
out,
"| {v} | {} | {works} | {} | {} | {} | {} | `{}` {}% |",
cells.len(),
n("runs"),
n("builds"),
n("fails"),
cells.iter().filter(|e| e.flaky).count(),
bar(works, cells.len(), 10),
percent(works, cells.len())
);
}
}

/// The status section, markers included.
#[must_use]
#[allow(clippy::too_many_lines)]
Expand Down Expand Up @@ -216,10 +260,24 @@ pub fn section(repo: &Repo, m: &Matrix, date: &str) -> String {
.map(|v| {
let n = latest.iter().filter(|e| e.verdict == *v).count();
#[allow(clippy::cast_precision_loss)]
((*v).to_owned(), n as f64, n.to_string())
(
format!("{} {v}", crate::publish::letter(v)),
n as f64,
n.to_string(),
)
})
.collect();
chart(&mut out, &verdicts);
out.push_str("\nThe same cells by the rung they reached (spec 02.2), L8 being clean all the way through KUnit and the splat check.\n\n");
let rungs: Vec<(String, f64, String)> = (0..=8)
.map(|r| {
let rung = format!("L{r}");
let n = latest.iter().filter(|e| e.rung == rung).count();
#[allow(clippy::cast_precision_loss)]
(rung, n as f64, n.to_string())
})
.collect();
chart(&mut out, &rungs);

let mut gccs: Vec<(&str, Version)> = repo
.gccs
Expand Down Expand Up @@ -254,27 +312,33 @@ pub fn section(repo: &Repo, m: &Matrix, date: &str) -> String {
chart(&mut out, &times);
}

by_gcc(&mut out, &gccs, &latest);

let mut recent = latest.clone();
recent.sort_by_key(|e| std::cmp::Reverse(e.started));
if !recent.is_empty() {
out.push_str("\n### Latest cells\n\n| Date | Kernel | GCC | Platform | Config | Verdict | Rung | Minutes |\n|---|---|---|---|---|---|---|--:|\n");
for e in recent.iter().take(10) {
let _ = writeln!(
out,
"| {} | {} | {} | {} | {} | {} | {} | {:.0} |",
"| {} | {} | {} | {} | {} | {} {} | {} | {:.0} |",
e.date,
e.kernel,
e.gcc.trim_start_matches("gcc-"),
e.platform,
e.config,
square(e),
e.verdict,
e.rung,
e.seconds / 60.0
);
}
}

out.push_str("\n### Matrices\n\nOne letter per cell: W works, R runs, B builds, F fails, · n/a, and blank where the cell has not run yet. A `*` marks a flaky cell, whose boots disagreed and which keeps the lowest. A table appears once its first cell has run, and then every Current kernel has a row in it. The full heat maps, with warning counts, are in [reports](reports).\n");
let _ = write!(
out,
"\n### Matrices\n\nOne square per cell: {LEGEND} A table appears once its first cell has run, and then every Current kernel has a row in it. The full heat maps, with warning counts, are in [reports](reports).\n"
);
let current: Vec<Version> = repo
.kernels
.in_set("current")
Expand Down Expand Up @@ -329,9 +393,7 @@ pub fn section(repo: &Repo, m: &Matrix, date: &str) -> String {
p.name,
count(ran.len(), "cell")
);
table(&mut out, &cells, config, &kernels, &cols, |e| {
format!("{}{}", letter(&e.verdict), if e.flaky { "*" } else { "" })
});
table(&mut out, &cells, config, &kernels, &cols, square);
}
}
out.push_str(END);
Expand All @@ -354,12 +416,15 @@ mod tests {
use std::path::Path;

#[test]
fn bars_fill_in_proportion() {
assert_eq!(bar(0, 10, 10), "..........");
assert_eq!(bar(1, 10, 10), "#.........");
assert_eq!(bar(1, 504, 30), "#.............................");
assert_eq!(bar(10, 10, 10), "##########");
assert_eq!(bar(0, 0, 4), "....");
fn bars_fill_in_eighths() {
assert_eq!(bar(0, 10, 10), "░░░░░░░░░░");
assert_eq!(bar(1, 10, 10), "█░░░░░░░░░");
assert_eq!(bar(1, 504, 30), "▏░░░░░░░░░░░░░░░░░░░░░░░░░░░░░");
assert_eq!(bar(3, 16, 4), "▊░░░");
assert_eq!(bar(1, 2, 3), "█▌░");
assert_eq!(bar(10, 10, 10), "██████████");
assert_eq!(bar(0, 0, 4), "░░░░");
assert_eq!(blocks(9, 3, ' '), "█▏ ");
}

#[test]
Expand All @@ -382,7 +447,8 @@ mod tests {
let s = section(&repo, &m, "2026-10-02");
assert!(s.starts_with(BEGIN) && s.trim_end().ends_with(END));
assert!(s.contains("x86_64 defconfig+gk"));
assert!(s.contains("works "));
assert!(s.contains("🟩 works"));
assert!(s.contains("| GCC | Cells |"));
let current = repo.kernels.in_set("current");
for k in current {
assert!(s.contains(&format!("| {} |", k.version)), "{}", k.version);
Expand Down
Loading