diff --git a/Cargo.lock b/Cargo.lock index 1e18b1d023b..7c192d6e676 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1096,6 +1096,10 @@ dependencies = [ name = "toyos-libc-copies" version = "0.1.0" +[[package]] +name = "toyos-llvmbar" +version = "0.1.0" + [[package]] name = "toyos-logstream" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 661c2ad735b..7a4dda16e5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ members = [ "toyos-inspect", "toyos-keymap", "toyos-ld", + "toyos-llvmbar", "toyos-libc-copies", "toyos-logstream", "toyos-manifest", diff --git a/issues/build/toyos-builds-itself.md b/issues/build/toyos-builds-itself.md index 8bf082ce50d..a12af500877 100644 --- a/issues/build/toyos-builds-itself.md +++ b/issues/build/toyos-builds-itself.md @@ -53,6 +53,94 @@ enough for an LLVM build tree memory beyond what 2 MiB process pages allow (`issues/kernel/process-memory-is-2-mib-pages-and-that-caps-the-process-count.md`). +**The bar: Linux on this T14 building the same LLVM with a clang-built +clang+lld against libc++.** It is a recipe, and a ToyOS run matches every +element of it. `toyos-llvmbar` holds its lines (`t14/block.txt`), the steps +the T14 runs (`t14/`), the power envelope and the judge, whose module header +is the rule a sample is valid by. + +- *Source*: `rust-lang/llvm-project` at + `52ed14fcd56afc30f9cccd8ca8ce237c2eef7e04`. A ToyOS run's source, for + the building compiler and the build alike, differs from it by nothing + but the fork's commits over that base (`forks.toml`, `[llvm-project]`). +- *Building compiler*: clang+lld at `52ed14fc`, Release, + `LLVM_ENABLE_LTO=OFF`, `LLVM_BUILD_INSTRUMENTED=OFF`, no profile data, + built by clang+lld at `52ed14fc` with the same configuration and flags: + the `s2` lines. Stage 1 is only how the first clang exists: gcc, which + has no ToyOS target, builds it with the `s1` lines + (`gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0`). For ToyOS it is M2's + cross build of the same source and configuration, whose lines M2 owes. +- *C++ library*: libc++ with libc++abi from the same source, built in each + stage's build by that stage's own clang (`runtimes`). The `s2` tools link + it statically: `s2`'s `clang-22` and `lld` name no C++ shared object and + reference no `GLIBCXX` symbol. `s2`'s libc++ headers are + `_LIBCPP_VERSION` 220108. +- *Build*: that clang+lld builds clang+lld from the same source with the + `s3` lines, configured afresh; cmake 3.28.3, ninja 1.11.1. +- *Timed span*: the last line's `ninja` alone. +- *Warm*: a span's configure line starts as soon as a complete `s3` + `ninja` line exits, and its `ninja` as soon as that configure exits; the + build reads no block from a block device during the span (`time -v`'s + file system inputs 0). +- *Power envelope*: `toyos-llvmbar`'s `ENVELOPE` and `ENVELOPE_EVERY_CPU`, + every element with its value; `LINUX` and `LINUX_EVERY_CPU` are how Linux + reaches them. +- *Machine*: i5-1135G7, 8 threads, 16476082176 B RAM; microcode revision + `0xbe` on every CPU; Ubuntu 24.04.4 with the kernel + `issues/hardware/linuxs-readings-of-the-t14-and-the-tcg-model-are-not-committed.md` + pins, 6.8.0-142-generic; ext4 on LVM. +- *Mitigations*: that issue's T14 capture is the one read of them. A sample + reads the kernel, the vulnerabilities and the microcode at its start and + its end with the capture's own commands, and the judge refuses it unless + they are the capture's. + +The bar is the best of three valid samples, and it is not set: no sample is +valid by the judge. + +Owed once the T14 answers again +(`issues/hardware/a-t14-measurement-has-no-way-back-when-the-t14-stops-answering.md`), +and Ubuntu is wiped only after the capture and the bar are both committed: + +- the capture on `main`, whose `/proc/version`, vulnerabilities and + `/proc/cpuinfo` text is the judge's second argument; +- `t14/driver.sh` under `t14/sampler.sh`, run until `toyos-llvmbar` exits + 0, and the bar, the BIOS version and the kernel command line it prints + recorded here; +- *Flags*, of `s2` and `s3`: the `CMAKE_*_FLAGS` and linker flags the + run's `*-cache.txt` hold, and the compile line `s3-commands.txt` holds + for `clang/tools/driver/driver.cpp` and the link line for `clang`, which + show whether `$LIBCXX` added `-stdlib=libc++` and `-static-libstdc++`; +- `s2-commands.txt` and `s3-commands.txt` compared: they may differ only in + the build directory. + +*Exit*: a ToyOS build of the source above with the `s3` configuration and +flags, by a clang+lld built by the recipe above, on this T14, warm, whose +`ninja` exits 0 at or under the bar, read back as the judge reads a Linux +sample: the envelope at its cadence from a start read to an end read, and at +the span's start and end the microcode, the BIOS version (SMBIOS type 0) +equal to the Linux sample's, and for each entry the capture shows as a +mitigation the line +`issues/kernel/each-boot-prints-the-vulnerabilities-lines-linux-prints.md` +requires, with its mechanism in force on every CPU: + +- `gather_data_sampling`: `IA32_MCU_OPT_CTRL` (0x123) `GDS_MITG_DIS` clear + (`issues/kernel/spec-ctrl-and-gds-stay-as-firmware-left-them.md`); +- `spectre_v2`: `IA32_SPEC_CTRL` (0x48) at the capture's read with SSBD + masked (the same issue); +- `indirect_target_selection`: the live thunk bodies equal to the body + `issues/kernel/a-pure-function-decides-a-cpus-speculation-mitigations-as-linux-does.md` + selects (`issues/kernel/indirect-branches-and-returns-run-without-thunks.md`); +- `spectre_v1`: CR4.SMAP set + (`issues/kernel/user-pointer-checks-have-no-spectre-v1-fence-and-smap-is-optional.md`); +- `spec_store_bypass`: where the capture shows Linux's prctl mode, SSBD is + set only in a process that asked, so parity is SSBD set in exactly the + build's processes that ask Linux, read on the switch into each + (`issues/kernel/no-program-runs-with-speculative-store-bypass-disabled.md`); + which of them ask is owed. + +An entry the capture shows as a mitigation that this list does not name holds +the exit until it is named here. + **What M2 must do to delete toyos-ld.** It is frozen and links nothing the host builds; what keeps it is that it is the one linker a ToyOS process can run, shipped as `/system/bin/toyos-ld` by `system.toml`'s `[programs]` row and named diff --git a/issues/hardware/a-t14-measurement-has-no-way-back-when-the-t14-stops-answering.md b/issues/hardware/a-t14-measurement-has-no-way-back-when-the-t14-stops-answering.md new file mode 100644 index 00000000000..6db17500788 --- /dev/null +++ b/issues/hardware/a-t14-measurement-has-no-way-back-when-the-t14-stops-answering.md @@ -0,0 +1,20 @@ +--- +status: open +kind: tooling +opened: 2026-09-28 +--- + +# A T14 measurement has no way back when the T14 stops answering + +A measurement under the T14's Ubuntu, such as the LLVM bar in +`issues/build/toyos-builds-itself.md`, is driven and read over ssh to +`t14@192.168.1.46`. At about 19:20Z on 2026-09-28, during the bar's `s3-2` +span, the T14 stopped answering ping and ssh, so the run's state, and whether +its driver and root sampler still run, cannot be read. Which of the T14's +links that address is on is unread; `ip route get` to the development Mac, +run on the T14, reads it. + +Owner: the orchestrator, which runs the T14 +(`issues/hardware/the-t14-answers-only-through-a-usb-stick.md`). Exit: a second +path to the T14 under Ubuntu, independent of the link that address is on, that +the orchestrator reaches when that link is down. diff --git a/toyos-llvmbar/Cargo.toml b/toyos-llvmbar/Cargo.toml new file mode 100644 index 00000000000..48daa3ffef9 --- /dev/null +++ b/toyos-llvmbar/Cargo.toml @@ -0,0 +1,13 @@ +# A member of the host workspace (root `Cargo.toml`): the T14 LLVM bar's steps +# and its judge. + +[package] +name = "toyos-llvmbar" +description = "The T14 LLVM bar: the steps its Linux run takes on the T14, and the judge of the files they write." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +[lints.rust] +warnings = "deny" diff --git a/toyos-llvmbar/src/lib.rs b/toyos-llvmbar/src/lib.rs new file mode 100644 index 00000000000..cee142ba1a0 --- /dev/null +++ b/toyos-llvmbar/src/lib.rs @@ -0,0 +1,597 @@ +//! The judge of the T14 LLVM bar (`issues/build/toyos-builds-itself.md`). +//! +//! On the T14, `t14/sampler.sh ` runs as root and `t14/driver.sh ` as +//! the build's user, `` holding `t14/block.txt`. The judge reads what they +//! leave there, and the T14 capture of +//! `issues/hardware/linuxs-readings-of-the-t14-and-the-tcg-model-are-not-committed.md`: +//! its `/proc/version`, vulnerabilities and `/proc/cpuinfo` text. +//! +//! A stage-3 span `s3-`, `k >= 1`, is a valid sample when: +//! - `samples-s3-.log` opens with a `read=start` row and closes with a +//! `read=end` row, every row between is `read=timed`, and every row is +//! `phase=s3-`; +//! - every row reads every element of [`ENVELOPE`], [`ENVELOPE_EVERY_CPU`], +//! [`LINUX`] and [`LINUX_EVERY_CPU`] at its value, and [`MICROCODE`] on every +//! CPU; +//! - consecutive rows start at most [`MAX_GAP_MS`] apart; +//! - `s3--time.txt`, GNU `time -v`'s, reads exit status 0 and file system +//! inputs 0; +//! - `s3--machine-start.txt` and `s3--machine-end.txt` both read kernel +//! [`KERNEL`], one command line, one BIOS version and one boot, and the +//! capture's `/proc/version`, vulnerabilities and microcode lines; +//! - it is warm: `s3-` exited 0, and its end read precedes this span's +//! start read. +//! +//! The bar is the shortest wall of the valid samples once there are +//! [`SAMPLES`] of them. + +use std::collections::BTreeMap; + +/// The T14's logical CPUs, every one of which a per-CPU element is read on. +pub const CPUS: usize = 8; +/// How many valid samples the bar is the best of. +pub const SAMPLES: usize = 3; +/// The longest a span may go between the starts of two reads. +pub const MAX_GAP_MS: u64 = 61_000; +/// The kernel the capture pins. +pub const KERNEL: &str = "6.8.0-142-generic"; +/// The microcode revision, `IA32_BIOS_SIGN_ID` (0x8B) bits 63:32, on every CPU. +pub const MICROCODE: u32 = 0xbe; + +/// The power envelope's package elements, a ToyOS run's as much as Linux's: +/// the sampler's key and the value it reads. +pub const ENVELOPE: &[(&str, &str)] = &[ + ("ac", "1"), + ("platform_profile", "performance"), + // MSR_PKG_POWER_LIMIT: PL1 64 W over 28 s, PL2 64 W over 2.44 ms, both + // enabled, unlocked. + ("msr_610", "0042820000dd8200"), + // The package limit through MMIO, MCHBAR + 0x59A0: PL1 20 W over 28 s, PL2 + // 64 W over 2.44 ms. + ("mmio_59a0", "0042820000dd80a0"), + // MSR_VR_CURRENT_CONFIG: the peak limit, 121 W. + ("msr_601", "00000000000003c8"), + // IA32_HWP_REQUEST_PKG. + ("msr_772", "000000008000ff01"), +]; + +/// The power envelope's per-CPU elements, each read on every CPU. +pub const ENVELOPE_EVERY_CPU: &[(&str, &str)] = &[ + // IA32_PM_ENABLE: HWP on. + ("msr_770", "0000000000000001"), + // IA32_HWP_REQUEST: minimum 4, maximum 42, desired 0, EPP 128, activity + // window 0, package control off. + ("msr_774", "0000000080002a04"), + // IA32_ENERGY_PERF_BIAS. + ("msr_1b0", "0000000000000006"), + // IA32_MISC_ENABLE bit 38 clear: turbo enabled. + ("msr_1a0_b38", "0"), +]; + +/// How Linux reaches the envelope, read back beside it: powercap's limits in +/// µW (PL1, PL2, peak) and windows in µs (PL1, PL2) for the MSR and the MMIO +/// package limit, and intel_pstate. +pub const LINUX: &[(&str, &str)] = &[ + ("rapl_msr", "64000000,64000000,121000000,27983872,2440"), + ("rapl_mmio", "20000000,64000000,121000000,27983872,2440"), + ("pstate", "active"), + ("no_turbo", "0"), + ("hwp_dynamic_boost", "0"), +]; + +/// Linux's per-CPU cpufreq settings, each read on every CPU. +pub const LINUX_EVERY_CPU: &[(&str, &str)] = &[ + ("governor", "powersave"), + ("epp", "balance_performance"), + ("min_khz", "400000"), + ("max_khz", "4200000"), +]; + +const VULNERABILITY: &str = "/sys/devices/system/cpu/vulnerabilities/"; +const BIOS: &str = "/sys/class/dmi/id/bios_version:"; +const BOOT: &str = "/proc/sys/kernel/random/boot_id:"; + +/// Every key a sampler row must carry and the value it must read. +pub fn wanted() -> Vec<(&'static str, String)> { + let every_cpu = |v: &str| [v; CPUS].join(","); + let mut want: Vec<(&'static str, String)> = ENVELOPE + .iter() + .chain(LINUX) + .map(|&(k, v)| (k, v.to_string())) + .collect(); + want.extend( + ENVELOPE_EVERY_CPU + .iter() + .chain(LINUX_EVERY_CPU) + .map(|&(k, v)| (k, every_cpu(v))), + ); + want.push(("msr_8b", every_cpu(&format!("{MICROCODE:08x}00000000")))); + want +} + +/// What Linux's T14 capture read that a span's machine reads must repeat. +#[derive(Debug)] +pub struct Capture { + version: String, + vulnerabilities: Vec, + microcode: Vec, +} + +impl Capture { + /// Takes the `/proc/version`, vulnerabilities and microcode lines out of + /// the capture's text, whatever else it holds. + pub fn parse(text: &str) -> Result { + let versions: Vec<&str> = text + .lines() + .filter(|l| l.starts_with("Linux version ")) + .collect(); + let [version] = versions[..] else { + return Err(format!( + "the capture holds {} /proc/version lines, not 1", + versions.len() + )); + }; + let mut vulnerabilities: Vec = text + .lines() + .filter(|l| l.starts_with(VULNERABILITY)) + .map(String::from) + .collect(); + vulnerabilities.sort(); + if vulnerabilities.is_empty() { + return Err("the capture holds no vulnerabilities line".into()); + } + let microcode: Vec = text.lines().filter_map(microcode).collect(); + if microcode.len() != CPUS { + return Err(format!( + "the capture holds {} microcode lines, not {CPUS}", + microcode.len() + )); + } + Ok(Capture { + version: version.into(), + vulnerabilities, + microcode, + }) + } +} + +/// One machine read, as `driver.sh`'s `machine` writes it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Machine { + pub version: String, + pub cmdline: String, + pub bios: String, + boot: String, + vulnerabilities: Vec, + microcode: Vec, +} + +impl Machine { + pub fn parse(text: &str) -> Result { + let mut lines = text.lines(); + let version = lines + .next() + .filter(|l| l.starts_with("Linux version ")) + .ok_or("its first line is not /proc/version")?; + let cmdline = lines + .next() + .filter(|l| !l.is_empty()) + .ok_or("its second line is not /proc/cmdline")?; + let (mut vulnerabilities, mut microcodes, mut bios, mut boot) = + (Vec::new(), Vec::new(), None, None); + for line in lines { + if line.starts_with(VULNERABILITY) { + vulnerabilities.push(line.to_string()); + } else if let Some(m) = microcode(line) { + microcodes.push(m); + } else if let Some(b) = line.strip_prefix(BIOS) { + once(&mut bios, b, "BIOS version")?; + } else if let Some(b) = line.strip_prefix(BOOT) { + once(&mut boot, b, "boot_id")?; + } else { + return Err(format!("it holds a line nothing reads: {line:?}")); + } + } + vulnerabilities.sort(); + if vulnerabilities.is_empty() { + return Err("it holds no vulnerabilities line".into()); + } + if microcodes.len() != CPUS { + return Err(format!( + "it holds {} microcode lines, not {CPUS}", + microcodes.len() + )); + } + Ok(Machine { + version: version.into(), + cmdline: cmdline.into(), + bios: bios.ok_or("it holds no BIOS version")?, + boot: boot.ok_or("it holds no boot_id")?, + vulnerabilities, + microcode: microcodes, + }) + } +} + +fn once(slot: &mut Option, value: &str, what: &str) -> Result<(), String> { + match slot.replace(value.to_string()) { + None if !value.is_empty() => Ok(()), + None => Err(format!("its {what} is empty")), + Some(_) => Err(format!("it holds two {what} lines")), + } +} + +/// The value of a `/proc/cpuinfo` `microcode` line. +fn microcode(line: &str) -> Option { + let value = line + .strip_prefix("microcode")? + .trim_start() + .strip_prefix(':')?; + Some(value.trim().to_string()) +} + +/// Milliseconds since the epoch of a `date -u +%FT%T.%3NZ` stamp. +pub fn utc_ms(stamp: &str) -> Result { + let bad = || format!("{stamp:?} is not a UTC stamp"); + let b = stamp.as_bytes(); + let separators = [ + (4, b'-'), + (7, b'-'), + (10, b'T'), + (13, b':'), + (16, b':'), + (19, b'.'), + (23, b'Z'), + ]; + if b.len() != 24 || separators.iter().any(|&(i, c)| b[i] != c) { + return Err(bad()); + } + let n = |from: usize, to: usize| -> Result { + let digits = &stamp[from..to]; + if digits.bytes().all(|c| c.is_ascii_digit()) { + digits.parse().map_err(|_| bad()) + } else { + Err(bad()) + } + }; + let (y, m, d) = (n(0, 4)?, n(5, 7)?, n(8, 10)?); + let (hh, mm, ss, ms) = (n(11, 13)?, n(14, 16)?, n(17, 19)?, n(20, 23)?); + if !(1..=12).contains(&m) || !(1..=31).contains(&d) || hh > 23 || mm > 59 || ss > 60 { + return Err(bad()); + } + // Days from 1970-01-01 in the proleptic Gregorian calendar, the year + // taken to start in March so the leap day ends it. + let y = if m <= 2 { y - 1 } else { y }; + let era = y.div_euclid(400); + let yoe = y - era * 400; + let doy = (153 * ((m + 9) % 12) + 2) / 5 + d - 1; + let days = era * 146_097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719_468; + u64::try_from(((days * 24 + hh) * 60 + mm) * 60 + ss) + .map(|s| s * 1000 + ms as u64) + .map_err(|_| bad()) +} + +/// Centiseconds of GNU time's `Elapsed (wall clock)` value, `m:ss.cc` or +/// `h:mm:ss.cc`. +pub fn wall_cs(value: &str) -> Result { + let bad = || format!("{value:?} is not a GNU time wall clock"); + let (whole, cc) = value.split_once('.').ok_or_else(bad)?; + let numbers: Vec = whole + .split(':') + .chain([cc]) + .map(|p| { + if !p.is_empty() && p.bytes().all(|c| c.is_ascii_digit()) { + p.parse().ok() + } else { + None + } + }) + .collect::>() + .ok_or_else(bad)?; + let (h, m, s, c) = match numbers[..] { + [m, s, c] => (0, m, s, c), + [h, m, s, c] => (h, m, s, c), + _ => return Err(bad()), + }; + if cc.len() != 2 || s > 59 || (h > 0 && m > 59) { + return Err(bad()); + } + Ok(((h * 60 + m) * 60 + s) * 100 + c) +} + +/// `wall_cs`'s inverse, in GNU time's form. +pub fn show_wall(cs: u64) -> String { + let (s, c) = (cs / 100, cs % 100); + match s / 3600 { + 0 => format!("{}:{:02}.{c:02}", s / 60, s % 60), + h => format!("{h}:{:02}:{:02}.{c:02}", s / 60 % 60, s % 60), + } +} + +/// A row's `key=value` fields. +fn fields(row: &str) -> Result, String> { + let mut map = BTreeMap::new(); + for token in row.split_whitespace() { + let (k, v) = token + .split_once('=') + .ok_or_else(|| format!("{token:?} is not key=value"))?; + if map.insert(k, v).is_some() { + return Err(format!("{k} twice")); + } + } + Ok(map) +} + +/// The rows of one span's sampler log against the envelope, its tags and its +/// cadence; the start read's time when the start and end reads are both there. +fn check_log(text: &str, span: &str, refusals: &mut Vec) -> Option { + let want = wanted(); + let rows: Vec<&str> = text.lines().collect(); + if rows.len() < 2 { + refusals.push(format!( + "its sampler log holds {} rows, and a span is read at its start and its end", + rows.len() + )); + } + let (mut first, mut prev, mut ends): (Option, Option, _) = + (None, None, (false, false)); + for (i, row) in rows.iter().enumerate() { + let n = i + 1; + let f = match fields(row) { + Ok(f) => f, + Err(e) => { + refusals.push(format!("row {n}: {e}")); + continue; + } + }; + let tag = match i { + 0 => "start", + _ if n == rows.len() => "end", + _ => "timed", + }; + match f.get("read") { + Some(&t) if t == tag => { + ends.0 |= tag == "start"; + ends.1 |= tag == "end"; + } + got => refusals.push(format!( + "row {n}: read={}, not read={tag}", + got.unwrap_or(&"") + )), + } + if f.get("phase") != Some(&span) { + refusals.push(format!( + "row {n}: phase={}, not phase={span}", + f.get("phase").unwrap_or(&"") + )); + } + for (k, v) in &want { + if f.get(k) != Some(&v.as_str()) { + refusals.push(format!( + "row {n}: {k}={}, not {v}", + f.get(k).unwrap_or(&"") + )); + } + } + match f.get("utc").map(|u| utc_ms(u)) { + Some(Ok(t)) => { + if let Some(p) = prev { + match t.checked_sub(p) { + Some(gap) if gap <= MAX_GAP_MS => {} + Some(gap) => { + refusals.push(format!("row {n}: {gap} ms after the read before it")) + } + None => refusals.push(format!("row {n}: earlier than the read before it")), + } + } + first.get_or_insert(t); + prev = Some(t); + } + Some(Err(e)) => refusals.push(format!("row {n}: {e}")), + None => refusals.push(format!("row {n}: no utc")), + } + } + first.filter(|_| ends == (true, true)) +} + +/// GNU `time -v`'s `key: value` lines. +fn gnu_time(text: &str) -> BTreeMap<&str, &str> { + text.lines() + .filter_map(|l| l.rsplit_once(": ")) + .map(|(k, v)| (k.trim(), v.trim())) + .collect() +} + +/// Exit status 0 and file system inputs 0, or why not; the wall if both hold. +fn check_time(text: Option<&str>, name: &str, refusals: &mut Vec) -> Option { + let Some(text) = text else { + refusals.push(format!("{name} has no time file: its ninja did not finish")); + return None; + }; + let time = gnu_time(text); + let mut ok = true; + for (key, want) in [("Exit status", "0"), ("File system inputs", "0")] { + if time.get(key) != Some(&want) { + refusals.push(format!( + "{name}: {key} {}, not {want}", + time.get(key).unwrap_or(&"") + )); + ok = false; + } + } + match time + .get("Elapsed (wall clock) time (h:mm:ss or m:ss)") + .map(|w| wall_cs(w)) + { + Some(Ok(wall)) if ok => Some(wall), + Some(Ok(_)) => None, + Some(Err(e)) => { + refusals.push(format!("{name}: {e}")); + None + } + None => { + refusals.push(format!("{name}: no wall clock")); + None + } + } +} + +/// One stage-3 span's judgement. +#[derive(Debug)] +pub struct Verdict { + pub span: String, + pub wall_cs: Option, + pub machine: Option, + pub refusals: Vec, +} + +impl Verdict { + pub fn valid(&self) -> bool { + self.refusals.is_empty() && self.wall_cs.is_some() + } +} + +/// Judges `s3-`, `k >= 1`, reading the run directory through `read`, which +/// answers `None` for a file that is not there. +pub fn judge_span(read: &dyn Fn(&str) -> Option, capture: &Capture, k: u32) -> Verdict { + assert!( + k >= 1, + "s3-0 is the line the first span follows, never a sample" + ); + let span = format!("s3-{k}"); + let prev = format!("s3-{}", k - 1); + let mut refusals = Vec::new(); + + let start = match read(&format!("samples-{span}.log")) { + Some(log) => check_log(&log, &span, &mut refusals), + None => { + refusals.push("no sampler log".into()); + None + } + }; + let wall_cs = check_time( + read(&format!("{span}-time.txt")).as_deref(), + &span, + &mut refusals, + ); + + let mut reads = Vec::new(); + for end in ["start", "end"] { + let name = format!("{span}-machine-{end}.txt"); + match read(&name).map(|t| Machine::parse(&t)) { + Some(Ok(m)) => reads.push(m), + Some(Err(e)) => refusals.push(format!("{name}: {e}")), + None => refusals.push(format!("no {name}")), + } + } + for m in &reads { + if m.version.split_whitespace().nth(2) != Some(KERNEL) { + refusals.push(format!("the kernel is not {KERNEL}: {:?}", m.version)); + } + let each = format!("{MICROCODE:#x}"); + let checks = [ + ( + m.version == capture.version, + "its /proc/version is not the capture's", + ), + ( + m.vulnerabilities == capture.vulnerabilities, + "its vulnerabilities lines are not the capture's", + ), + ( + m.microcode == capture.microcode, + "its microcode lines are not the capture's", + ), + ( + m.microcode.iter().all(|r| *r == each), + "a CPU's microcode is not the recipe's", + ), + ]; + refusals.extend( + checks + .iter() + .filter(|(ok, _)| !ok) + .map(|(_, why)| why.to_string()), + ); + } + if let [first, last] = &reads[..] { + for (same, what) in [ + (first.cmdline == last.cmdline, "command line"), + (first.bios == last.bios, "BIOS version"), + (first.boot == last.boot, "boot"), + ] { + if !same { + refusals.push(format!( + "the {what} changed between the start and end machine reads" + )); + } + } + } + + let prev_exited = check_time( + read(&format!("{prev}-time.txt")).as_deref(), + &prev, + &mut refusals, + ) + .is_some(); + let prev_end = read(&format!("samples-{prev}.log")).and_then(|log| { + let f = fields(log.lines().last()?).ok()?; + if f.get("read") != Some(&"end") { + return None; + } + utc_ms(f.get("utc")?).ok() + }); + let prev_end = prev_end.filter(|_| prev_exited); + match (prev_end, start) { + (Some(p), Some(start)) if p < start => {} + (Some(_), Some(_)) => refusals.push(format!( + "{prev}'s end read does not precede this span's start read" + )), + (None, _) => refusals.push(format!( + "not warm: {prev} has no end read after a build that exited 0" + )), + (Some(_), None) => {} + } + + Verdict { + span, + wall_cs, + machine: reads.into_iter().next(), + refusals, + } +} + +/// Every stage-3 span a run left, judged, and the bar if it is set. +pub struct Run { + pub verdicts: Vec, + pub bar_cs: Option, +} + +/// Judges `s3-1` onwards until a span that left no file at all. +pub fn judge(read: &dyn Fn(&str) -> Option, capture: &Capture) -> Run { + let left_any = |k: u32| { + [ + format!("samples-s3-{k}.log"), + format!("s3-{k}-time.txt"), + format!("s3-{k}-machine-start.txt"), + ] + .iter() + .any(|f| read(f).is_some()) + }; + let verdicts: Vec = (1..) + .take_while(|&k| left_any(k)) + .map(|k| judge_span(read, capture, k)) + .collect(); + let walls: Vec = verdicts + .iter() + .filter(|v| v.valid()) + .filter_map(|v| v.wall_cs) + .collect(); + let bar_cs = (walls.len() >= SAMPLES) + .then(|| walls.iter().copied().min()) + .flatten(); + Run { verdicts, bar_cs } +} + +#[cfg(test)] +mod tests; diff --git a/toyos-llvmbar/src/main.rs b/toyos-llvmbar/src/main.rs new file mode 100644 index 00000000000..77d0cb9d3d7 --- /dev/null +++ b/toyos-llvmbar/src/main.rs @@ -0,0 +1,67 @@ +//! `toyos-llvmbar `: every stage-3 span's verdict +//! and the bar. Exits 0 when the bar is set, 1 when it is not, 2 when it +//! cannot judge. + +use std::path::Path; +use std::process::exit; + +use toyos_llvmbar::{judge, show_wall, Capture, SAMPLES}; + +fn refuse(why: String) -> ! { + eprintln!("toyos-llvmbar: {why}\nusage: toyos-llvmbar "); + exit(2) +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + let [dir, capture] = &args[..] else { + refuse(format!("two arguments, not {}", args.len())) + }; + let dir = Path::new(dir); + if !dir.is_dir() { + refuse(format!("{} is not a directory", dir.display())); + } + let capture = std::fs::read_to_string(capture) + .map_err(|e| format!("read {capture}: {e}")) + .and_then(|t| Capture::parse(&t)) + .unwrap_or_else(|why| refuse(why)); + let read = |name: &str| match std::fs::read_to_string(dir.join(name)) { + Ok(text) => Some(text), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, + Err(e) => panic!("read {}: {e}", dir.join(name).display()), + }; + let run = judge(&read, &capture); + for v in &run.verdicts { + if v.valid() { + println!( + "{} VALID {}", + v.span, + show_wall(v.wall_cs.expect("a valid span has a wall")) + ); + } else { + println!("{} INVALID", v.span); + for why in &v.refusals { + println!(" {why}"); + } + } + } + let valid = run.verdicts.iter().filter(|v| v.valid()).count(); + if let Some(m) = run + .verdicts + .iter() + .find(|v| v.valid()) + .and_then(|v| v.machine.as_ref()) + { + println!( + "machine: {}\n cmdline {}\n BIOS {}", + m.version, m.cmdline, m.bios + ); + } + match run.bar_cs { + Some(bar) => println!("bar: {}, the best of {valid} valid samples", show_wall(bar)), + None => { + println!("bar: not set, {valid} of {SAMPLES} valid samples"); + exit(1); + } + } +} diff --git a/toyos-llvmbar/src/tests.rs b/toyos-llvmbar/src/tests.rs new file mode 100644 index 00000000000..5b4b035bde1 --- /dev/null +++ b/toyos-llvmbar/src/tests.rs @@ -0,0 +1,415 @@ +use std::collections::BTreeMap; + +use super::*; + +/// A row the T14's sampler wrote during a stage-3 build, before rows carried +/// a `read` tag. +const T14_ROW: &str = concat!( + "utc=2026-09-28T18:20:04.802Z ", + "epoch=1790619604 ", + "phase=s3-1 ", + "temp_mc=70000 ", + "disk_rd_sectors=nvme0n1:1918110,sda:78852 ", + "ac=1 ", + "platform_profile=performance ", + "rapl_msr=64000000,64000000,121000000,27983872,2440 ", + "rapl_mmio=20000000,64000000,121000000,27983872,2440 ", + "msr_610=0042820000dd8200 ", + "mmio_59a0=0042820000dd80a0 ", + "msr_601=00000000000003c8 ", + "msr_770=0000000000000001,0000000000000001,0000000000000001,0000000000000001,0000000000000001,0000000000000001,0000000000000001,0000000000000001 ", + "msr_774=0000000080002a04,0000000080002a04,0000000080002a04,0000000080002a04,0000000080002a04,0000000080002a04,0000000080002a04,0000000080002a04 ", + "msr_1b0=0000000000000006,0000000000000006,0000000000000006,0000000000000006,0000000000000006,0000000000000006,0000000000000006,0000000000000006 ", + "msr_1a0_b38=0,0,0,0,0,0,0,0 ", + "msr_772=000000008000ff01 ", + "msr_8b=000000be00000000,000000be00000000,000000be00000000,000000be00000000,000000be00000000,000000be00000000,000000be00000000,000000be00000000 ", + "governor=powersave,powersave,powersave,powersave,powersave,powersave,powersave,powersave ", + "epp=balance_performance,balance_performance,balance_performance,balance_performance,balance_performance,balance_performance,balance_performance,balance_performance ", + "min_khz=400000,400000,400000,400000,400000,400000,400000,400000 ", + "max_khz=4200000,4200000,4200000,4200000,4200000,4200000,4200000,4200000 ", + "pstate=active ", + "no_turbo=0 ", + "hwp_dynamic_boost=0 ", + "energy_uj=107418320833", +); +const T14_UTC: &str = "utc=2026-09-28T18:20:04.802Z"; + +const VERSION: &str = "Linux version 6.8.0-142-generic (fixture) #142-Ubuntu SMP"; + +/// GNU `time -v`'s output for a ninja that ran `wall`. +fn time_v(wall: &str, exit: u32, inputs: u32) -> String { + format!( + "\tCommand being timed: \"ninja -C s3 -j8 clang lld\"\n\ + \tUser time (seconds): 19403.20\n\ + \tPercent of CPU this job got: 795%\n\ + \tElapsed (wall clock) time (h:mm:ss or m:ss): {wall}\n\ + \tMajor (requiring I/O) page faults: 417\n\ + \tFile system inputs: {inputs}\n\ + \tFile system outputs: 4145776\n\ + \tPage size (bytes): 4096\n\ + \tExit status: {exit}\n" + ) +} + +fn vulnerabilities() -> String { + [ + "gather_data_sampling:Mitigation: fixture", + "spectre_v1:Mitigation: fixture", + "meltdown:Not affected", + ] + .iter() + .map(|l| format!("{VULNERABILITY}{l}\n")) + .collect() +} + +fn microcodes() -> String { + format!("microcode\t: {MICROCODE:#x}\n").repeat(CPUS) +} + +fn machine(bios: &str, boot: &str) -> String { + format!( + "{VERSION}\nBOOT_IMAGE=/vmlinuz-{KERNEL} ro\n{}{}{BIOS}{bios}\n{BOOT}{boot}\n", + vulnerabilities(), + microcodes() + ) +} + +/// The capture's text: its lines among others it prints. +fn capture_text() -> String { + format!("Reading package lists...\n{VERSION}\nBOOT_IMAGE=/vmlinuz-{KERNEL} ro\n{}{}0x0000000000000000\n", vulnerabilities(), microcodes()) +} + +fn capture() -> Capture { + Capture::parse(&capture_text()).expect("the fixture's capture parses") +} + +/// The T14's row at `t` seconds into its day, for `span`, tagged `read`. +fn row(span: &str, t: u64, read: &str) -> String { + let stamp = format!( + "utc=2026-09-28T{:02}:{:02}:{:02}.500Z", + t / 3600, + t / 60 % 60, + t % 60 + ); + T14_ROW.replacen(T14_UTC, &stamp, 1).replacen( + "phase=s3-1 ", + &format!("phase={span} read={read} "), + 1, + ) +} + +/// A span's sampler log: a start read at `t`, timed reads 60 s apart, and an +/// end read 60 s after the last. +fn log(span: &str, t: u64, timed: u64) -> String { + let mut rows = vec![row(span, t, "start")]; + rows.extend((1..=timed).map(|i| row(span, t + 60 * i, "timed"))); + rows.push(row(span, t + 60 * (timed + 1), "end")); + rows.join("\n") + "\n" +} + +/// A run of s3-0 to s3-3 in which every span is valid. +fn run() -> BTreeMap { + let mut files = BTreeMap::new(); + for (k, wall) in ["42:37.88", "42:40.00", "42:36.03", "42:38.50"] + .iter() + .enumerate() + { + let span = format!("s3-{k}"); + files.insert( + format!("samples-{span}.log"), + log(&span, 3600 + 1000 * k as u64, 4), + ); + files.insert(format!("{span}-time.txt"), time_v(wall, 0, 0)); + files.insert( + format!("{span}-machine-start.txt"), + machine("BIOS-A", "boot-a"), + ); + files.insert( + format!("{span}-machine-end.txt"), + machine("BIOS-A", "boot-a"), + ); + } + files +} + +fn judged(files: &BTreeMap) -> Run { + judge(&|name: &str| files.get(name).cloned(), &capture()) +} + +fn edit(files: &mut BTreeMap, name: &str, f: impl FnOnce(&str) -> String) { + let text = files + .get(name) + .unwrap_or_else(|| panic!("the fixture has no {name}")); + let edited = f(text); + assert_ne!(&edited, text, "the edit to {name} changed nothing"); + files.insert(name.into(), edited); +} + +fn without_line(text: &str, which: fn(usize) -> usize) -> String { + let mut lines: Vec<&str> = text.lines().collect(); + lines.remove(which(lines.len())); + lines.join("\n") + "\n" +} + +/// The one span `edit` turned invalid, and what the judge said. +fn only_refused(files: &BTreeMap) -> (String, Vec) { + let run = judged(files); + let refused: Vec<&Verdict> = run.verdicts.iter().filter(|v| !v.valid()).collect(); + let [v] = refused[..] else { + panic!( + "{} spans refused, not 1: {:#?}", + refused.len(), + run.verdicts + ) + }; + assert_eq!(run.bar_cs, None, "two valid samples set no bar"); + (v.span.clone(), v.refusals.clone()) +} + +fn says(refusals: &[String], words: &str) -> bool { + refusals.iter().any(|r| r.contains(words)) +} + +#[test] +fn a_complete_run_sets_the_bar_to_its_shortest_valid_wall() { + let run = judged(&run()); + let spans: Vec<(&str, bool)> = run + .verdicts + .iter() + .map(|v| (v.span.as_str(), v.valid())) + .collect(); + assert_eq!( + spans, + [("s3-1", true), ("s3-2", true), ("s3-3", true)], + "{:#?}", + run.verdicts + ); + assert_eq!(run.bar_cs.map(show_wall).as_deref(), Some("42:36.03")); +} + +#[test] +fn a_span_without_its_start_read_is_refused() { + let mut files = run(); + edit(&mut files, "samples-s3-2.log", |t| without_line(t, |_| 0)); + let (span, why) = only_refused(&files); + assert_eq!(span, "s3-2"); + assert!(says(&why, "row 1: read=timed, not read=start"), "{why:#?}"); +} + +#[test] +fn a_span_without_its_end_read_is_refused() { + let mut files = run(); + edit(&mut files, "samples-s3-3.log", |t| { + without_line(t, |n| n - 1) + }); + let (span, why) = only_refused(&files); + assert_eq!(span, "s3-3"); + assert!(says(&why, "read=timed, not read=end"), "{why:#?}"); +} + +#[test] +fn a_span_cut_off_mid_build_is_refused() { + let mut files = run(); + edit(&mut files, "samples-s3-3.log", |t| { + without_line(t, |n| n - 1) + }); + files.remove("s3-3-time.txt"); + files.remove("s3-3-machine-end.txt"); + let (span, why) = only_refused(&files); + assert_eq!(span, "s3-3"); + assert!( + says(&why, "did not finish") && says(&why, "no s3-3-machine-end.txt"), + "{why:#?}" + ); +} + +#[test] +fn a_missing_timed_read_is_a_gap() { + let mut files = run(); + edit(&mut files, "samples-s3-1.log", |t| without_line(t, |_| 2)); + let (span, why) = only_refused(&files); + assert_eq!(span, "s3-1"); + assert!(says(&why, "120000 ms after the read before it"), "{why:#?}"); +} + +#[test] +fn one_element_off_on_one_cpu_is_refused() { + let mut files = run(); + edit(&mut files, "samples-s3-1.log", |t| { + t.replacen( + "0000000080002a04,0000000080002a04,0000000080002a04,0000000080002a04", + "0000000080002a04,0000000080002a04,0000000080002a04,0000000080002a05", + 1, + ) + }); + let (_, why) = only_refused(&files); + assert!(says(&why, "row 1: msr_774="), "{why:#?}"); +} + +#[test] +fn another_microcode_in_one_row_is_refused() { + let mut files = run(); + edit(&mut files, "samples-s3-2.log", |t| { + t.replacen("msr_8b=000000be00000000", "msr_8b=000000bc00000000", 1) + }); + let (_, why) = only_refused(&files); + assert!(says(&why, "row 1: msr_8b="), "{why:#?}"); +} + +#[test] +fn a_build_that_failed_or_read_a_block_is_refused() { + for (exit, inputs, words) in [(1, 0, "Exit status 1"), (0, 8, "File system inputs 8")] { + let mut files = run(); + edit(&mut files, "s3-3-time.txt", |_| { + time_v("42:36.03", exit, inputs) + }); + let (span, why) = only_refused(&files); + assert_eq!(span, "s3-3"); + assert!(says(&why, words), "{why:#?}"); + } +} + +#[test] +fn a_missing_machine_read_is_refused() { + for end in ["start", "end"] { + let mut files = run(); + files.remove(&format!("s3-2-machine-{end}.txt")); + let (_, why) = only_refused(&files); + assert!( + says(&why, &format!("no s3-2-machine-{end}.txt")), + "{why:#?}" + ); + } +} + +#[test] +fn the_machine_reads_agree_with_each_other_and_with_the_capture() { + type Change = fn(&str) -> String; + let cases: [(&str, Change, &str); 5] = [ + ( + "s3-1-machine-end.txt", + |_| machine("BIOS-B", "boot-a"), + "the BIOS version changed", + ), + ( + "s3-1-machine-end.txt", + |_| machine("BIOS-A", "boot-b"), + "the boot changed", + ), + ( + "s3-1-machine-start.txt", + |t| t.replacen("spectre_v1:Mitigation: fixture", "spectre_v1:Vulnerable", 1), + "vulnerabilities lines are not the capture's", + ), + ( + "s3-1-machine-start.txt", + |t| { + t.replacen( + "6.8.0-142-generic (fixture)", + "6.8.0-143-generic (fixture)", + 1, + ) + }, + "the kernel is not 6.8.0-142-generic", + ), + ( + "s3-1-machine-end.txt", + |t| t.replacen(": 0xbe", ": 0xbc", 1), + "microcode lines are not the capture's", + ), + ]; + for (name, change, words) in cases { + let mut files = run(); + edit(&mut files, name, change); + let (span, why) = only_refused(&files); + assert_eq!(span, "s3-1"); + assert!(says(&why, words), "{name}: {why:#?}"); + } +} + +#[test] +fn a_span_after_a_failed_build_is_not_warm() { + let mut files = run(); + edit(&mut files, "s3-0-time.txt", |_| time_v("42:37.88", 1, 0)); + let (span, why) = only_refused(&files); + assert_eq!(span, "s3-1"); + assert!(says(&why, "not warm: s3-0"), "{why:#?}"); +} + +#[test] +fn a_span_after_a_span_without_its_end_read_is_not_warm() { + let mut files = run(); + edit(&mut files, "samples-s3-1.log", |t| { + without_line(t, |n| n - 1) + }); + let run = judged(&files); + let refused: Vec<&str> = run + .verdicts + .iter() + .filter(|v| !v.valid()) + .map(|v| v.span.as_str()) + .collect(); + assert_eq!(refused, ["s3-1", "s3-2"]); + assert!( + says(&run.verdicts[1].refusals, "not warm: s3-1"), + "{:#?}", + run.verdicts[1] + ); +} + +#[test] +fn the_t14s_own_rows_read_the_envelope() { + let tagged = |t, read| row("s3-1", t, read); + let log = [tagged(3600, "start"), tagged(3660, "end")].join("\n"); + let mut refusals = Vec::new(); + assert!(check_log(&log, "s3-1", &mut refusals).is_some()); + assert_eq!(refusals, Vec::::new()); + + let untagged = check_log(&[T14_ROW, T14_ROW].join("\n"), "s3-1", &mut refusals); + assert!( + untagged.is_none() && says(&refusals, "read=, not read=start"), + "{refusals:#?}" + ); +} + +#[test] +fn a_utc_stamp_reads_as_date_does() { + let f = fields(T14_ROW).expect("the T14's row is key=value"); + let epoch: u64 = f["epoch"].parse().expect("epoch is a number"); + assert_eq!(utc_ms(f["utc"]).map(|ms| ms / 1000), Ok(epoch)); + assert_eq!(utc_ms("1970-01-01T00:00:00.000Z"), Ok(0)); + assert_eq!(utc_ms("2000-03-01T00:00:00.001Z"), Ok(951_868_800_001)); + assert!(utc_ms("2026-09-28 18:20:04.802Z").is_err()); +} + +#[test] +fn a_wall_clock_reads_both_of_gnu_times_forms() { + assert_eq!(wall_cs("42:36.03"), Ok(255_603)); + assert_eq!(wall_cs("1:02:03.45"), Ok(372_345)); + assert_eq!(show_wall(372_345), "1:02:03.45"); + for bad in ["42:36", "42:61.00", "42:36.3", "x:36.03"] { + assert!(wall_cs(bad).is_err(), "{bad}"); + } +} + +#[test] +fn a_capture_is_refused_without_one_version_or_every_cpus_microcode() { + assert!(Capture::parse(&format!("{}{VERSION}\n", capture_text())).is_err()); + assert!(Capture::parse(&capture_text().replacen("microcode\t: 0xbe\n", "", 1)).is_err()); + assert!(Capture::parse(&capture_text().replace(VULNERABILITY, "/elsewhere/")).is_err()); +} + +#[test] +fn the_sampler_writes_every_key_the_judge_reads() { + let sampler = include_str!("../t14/sampler.sh"); + for key in wanted() + .iter() + .map(|(k, _)| *k) + .chain(["utc", "phase", "read"]) + { + assert!( + sampler.contains(&format!(" {key}=$")) || sampler.contains(&format!("\"{key}=$")), + "sampler.sh writes no {key}" + ); + } +} diff --git a/toyos-llvmbar/t14/block.txt b/toyos-llvmbar/t14/block.txt new file mode 100644 index 00000000000..21bfee412b2 --- /dev/null +++ b/toyos-llvmbar/t14/block.txt @@ -0,0 +1,12 @@ +W=$HOME/llvm-baseline; SHA=52ed14fcd56afc30f9cccd8ca8ce237c2eef7e04 +mkdir -p $W; cd $W +git init -q src && git -C src remote add origin https://github.com/rust-lang/llvm-project.git +git -C src fetch -q --depth 1 origin $SHA && git -C src checkout -q FETCH_HEAD +CONF="-G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS=clang;lld -DLLVM_ENABLE_RUNTIMES=libcxx;libcxxabi;libunwind -DLIBCXX_STATICALLY_LINK_ABI_IN_STATIC_LIBRARY=ON -DLLVM_TARGETS_TO_BUILD=X86;AArch64 -DLLVM_ENABLE_ASSERTIONS=OFF -DLLVM_INCLUDE_TESTS=OFF -DLLVM_INCLUDE_BENCHMARKS=OFF -DLLVM_INCLUDE_EXAMPLES=OFF" +LIBCXX="-DLLVM_ENABLE_LIBCXX=ON -DLLVM_STATIC_LINK_CXX_STDLIB=ON" +rm -rf s1; cmake -S src/llvm -B s1 $CONF -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ > s1-config.log +/usr/bin/time -v -o s1-time.txt ninja -C s1 -j8 clang lld runtimes > s1-build.log +rm -rf s2; PATH=$W/s1/bin:$PATH cmake -S src/llvm -B s2 $CONF $LIBCXX -DCMAKE_C_COMPILER=$W/s1/bin/clang -DCMAKE_CXX_COMPILER=$W/s1/bin/clang++ -DLLVM_ENABLE_LLD=ON > s2-config.log +PATH=$W/s1/bin:$PATH /usr/bin/time -v -o s2-time.txt ninja -C s2 -j8 clang lld runtimes > s2-build.log +rm -rf s3; PATH=$W/s2/bin:$PATH cmake -S src/llvm -B s3 $CONF $LIBCXX -DCMAKE_C_COMPILER=$W/s2/bin/clang -DCMAKE_CXX_COMPILER=$W/s2/bin/clang++ -DLLVM_ENABLE_LLD=ON > s3-config.log +PATH=$W/s2/bin:$PATH /usr/bin/time -v -o s3-time.txt ninja -C s3 -j8 clang lld > s3-build.log diff --git a/toyos-llvmbar/t14/driver.sh b/toyos-llvmbar/t14/driver.sh new file mode 100644 index 00000000000..724f6c100ec --- /dev/null +++ b/toyos-llvmbar/t14/driver.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# As the build's user, with sampler.sh running on the same run directory, which holds +# block.txt: s1, s2, one complete s3 line, then the stage-3 spans s3-1 to s3-3 back to +# back. Every configure and ninja line is block.txt's own line, eval'd. +set -eu +R=$1; B=$R/block.txt +line() { + local l n + l=$(awk -v p="$1" 'index($0, p) == 1' $B); n=$(printf '%s\n' "$l" | grep -c .) + [ "$n" = 1 ] || { echo "block: $n lines start with '$1'" >&2; exit 1; } + printf '%s\n' "$l" +} +L_W=$(line 'W=$HOME/llvm-baseline;'); L_CONF=$(line 'CONF='); L_LIBCXX=$(line 'LIBCXX=') +L_C1=$(line 'rm -rf s1;'); L_B1=$(line '/usr/bin/time') +L_C2=$(line 'rm -rf s2;'); L_B2=$(line 'PATH=$W/s1/bin:$PATH /usr/bin/time') +L_C3=$(line 'rm -rf s3;'); L_B3=$(line 'PATH=$W/s2/bin:$PATH /usr/bin/time') +eval "$L_W"; cd $W; eval "$L_CONF"; eval "$L_LIBCXX" +[ "$(git -C src rev-parse HEAD)" = "$SHA" ] || { echo "src is not at $SHA"; exit 1; } +[ -z "$(git -C src status --porcelain)" ] || { echo "src is not clean"; exit 1; } +SPID=$(cat $R/sampler.pid) +exec 3<>$R/phase.fifo +phase() { + [ -d /proc/$SPID ] || { echo "sampler $SPID gone"; exit 1; } + echo "$1" >&3 + echo "== $1 $(date -u +%FT%T.%3NZ)" +} +trap 'echo stop >&3' EXIT +# The kernel, command line, mitigations and microcode by the commands of Linux's T14 capture +# (issues/hardware/linuxs-readings-of-the-t14-and-the-tcg-model-are-not-committed.md), then the +# BIOS and the boot. +machine() { + { cat /proc/version /proc/cmdline; grep . /sys/devices/system/cpu/vulnerabilities/*; grep microcode /proc/cpuinfo + grep -H . /sys/class/dmi/id/bios_version /proc/sys/kernel/random/boot_id; } > $R/$1.txt +} +KEYS='^(CMAKE_BUILD_TYPE|CMAKE_C_COMPILER|CMAKE_CXX_COMPILER|CMAKE_C_FLAGS|CMAKE_CXX_FLAGS|CMAKE_C_FLAGS_RELEASE|CMAKE_CXX_FLAGS_RELEASE|CMAKE_(EXE|SHARED|MODULE|STATIC)_LINKER_FLAGS(_RELEASE)?|LLVM_ENABLE_LTO|LLVM_BUILD_INSTRUMENTED|LLVM_PROFDATA_FILE|LLVM_ENABLE_LLD|LLVM_ENABLE_PROJECTS|LLVM_ENABLE_RUNTIMES|LLVM_ENABLE_LIBCXX|LLVM_STATIC_LINK_CXX_STDLIB|LIBCXX_STATICALLY_LINK_ABI_IN_STATIC_LIBRARY|LLVM_TARGETS_TO_BUILD|LLVM_ENABLE_ASSERTIONS|CXX_(COMPILER|LINKER)_SUPPORTS_(STATIC_)?STDLIB)[:=]' +run() { # name stage configure-line ninja-line + local name=$1 s=$2 + eval "$3" + grep -E "$KEYS" $s/CMakeCache.txt > $R/$name-cache.txt + grep -E "compiler identification|Linker detection|lld project|libc\+\+|WARNING" $s-config.log >> $R/$name-cache.txt || true + mv $s-config.log $R/$name-config.log + sync + machine $name-machine-start + phase $name-start + eval "$4" + phase $name-end + machine $name-machine-end + mv $s-time.txt $R/$name-time.txt; mv $s-build.log $R/$name-build.log + echo "== $name done: $(grep -E 'Elapsed|Exit status' $R/$name-time.txt | tr '\n' ' ')" +} +run s1 s1 "$L_C1" "$L_B1" +run s2 s2 "$L_C2" "$L_B2" +for f in clang clang++ ld.lld; do [ -x s2/bin/$f ] || { echo "s2/bin/$f missing"; exit 1; }; done +for N in 0 1 2 3; do run s3-$N s3 "$L_C3" "$L_B3"; done +ninja -C s2 -t commands clang lld > $R/s2-commands.txt +ninja -C s3 -t commands clang lld > $R/s3-commands.txt +echo "== end $(date -u +%FT%T.%3NZ)" diff --git a/toyos-llvmbar/t14/sampler.sh b/toyos-llvmbar/t14/sampler.sh new file mode 100644 index 00000000000..d2d898f596c --- /dev/null +++ b/toyos-llvmbar/t14/sampler.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# As root, beside driver.sh: reads every envelope element into samples-.log at each +# phase the driver writes to phase.fifo, tagged read=start or read=end, and every 60 s +# between, tagged read=timed. +set -eu +R=$1 +P=/sys/class/powercap +CPUS="0 1 2 3 4 5 6 7" +[ "$(ls -d /dev/cpu/[0-9]* | wc -l)" = 8 ] || { echo "expected 8 CPUs" >&2; exit 1; } +[ -e /dev/cpu/0/msr ] || { echo "no /dev/cpu/0/msr: modprobe msr" >&2; exit 1; } +T= +for h in /sys/class/hwmon/hwmon*; do [ "$(cat $h/name)" = coretemp ] && T=$h; done +[ "$(cat $T/temp1_label)" = "Package id 0" ] || { echo "no package temp" >&2; exit 1; } +mch=$(dd if=/sys/bus/pci/devices/0000:00:00.0/config bs=1 skip=$((0x48)) count=8 status=none | od -An -tx8 | tr -d ' ') +[ $((0x$mch & 1)) = 1 ] || { echo "MCHBAR disabled: $mch" >&2; exit 1; } +MMIO_PL=$(( (0x$mch & ~0x7fff) + 0x59a0 )) +rd() { dd if=/dev/cpu/$1/msr bs=8 count=1 skip=$(($2)) iflag=skip_bytes status=none | od -An -tx8 | tr -d ' '; } +percpu() { local v= c; for c in $CPUS; do v=$v${v:+,}$(rd $c $1); done; echo $v; } +b38() { local v= c x; for c in $CPUS; do x=$(rd $c 0x1a0); v=$v${v:+,}$(( (0x$x >> 38) & 1 )); done; echo $v; } +sysall() { local v= c; for c in $CPUS; do v=$v${v:+,}$(cat /sys/devices/system/cpu/cpu$c/cpufreq/$1); done; echo $v; } +rapl() { echo $(cat $P/$1/constraint_{0,1,2}_power_limit_uw) $(cat $P/$1/constraint_{0,1}_time_window_us) | tr ' ' ,; } +disks() { awk '$3 ~ /^(nvme[0-9]+n[0-9]+|sd[a-z]+)$/ {printf "%s%s:%s", s, $3, $6; s=","}' /proc/diskstats; } +[ -p $R/phase.fifo ] || mkfifo $R/phase.fifo +echo $$ > $R/sampler.pid +exec 3<>$R/phase.fifo +phase=idle; out=$R/samples-idle.log +sample() { + echo "utc=$(date -u +%FT%T.%3NZ) epoch=$(date +%s) phase=$phase read=$1 temp_mc=$(cat $T/temp1_input) disk_rd_sectors=$(disks)" \ + "ac=$(cat /sys/class/power_supply/AC/online) platform_profile=$(cat /sys/firmware/acpi/platform_profile)" \ + "rapl_msr=$(rapl intel-rapl:0) rapl_mmio=$(rapl intel-rapl-mmio:0)" \ + "msr_610=$(rd 0 0x610) mmio_59a0=$(dd if=/dev/mem bs=8 count=1 skip=$MMIO_PL iflag=skip_bytes status=none | od -An -tx8 | tr -d ' ') msr_601=$(rd 0 0x601)" \ + "msr_770=$(percpu 0x770) msr_774=$(percpu 0x774) msr_1b0=$(percpu 0x1b0) msr_1a0_b38=$(b38) msr_772=$(rd 0 0x772) msr_8b=$(percpu 0x8b)" \ + "governor=$(sysall scaling_governor) epp=$(sysall energy_performance_preference) min_khz=$(sysall scaling_min_freq) max_khz=$(sysall scaling_max_freq)" \ + "pstate=$(cat /sys/devices/system/cpu/intel_pstate/status) no_turbo=$(cat /sys/devices/system/cpu/intel_pstate/no_turbo) hwp_dynamic_boost=$(cat /sys/devices/system/cpu/intel_pstate/hwp_dynamic_boost)" \ + "energy_uj=$(cat $P/intel-rapl:0/energy_uj)" >> "$out" +} +sample timed; next=$(($(date +%s) + 60)) +while :; do + wait_s=$((next - $(date +%s))); [ $wait_s -ge 1 ] || wait_s=1 + if read -t $wait_s -u 3 line; then + case $line in + *-start) phase=${line%-start}; out=$R/samples-$phase.log; sample start ;; + *-end) sample end; phase=idle; out=$R/samples-idle.log ;; + stop) sample timed; exit 0 ;; + *) echo "unknown phase: $line" >&2; exit 1 ;; + esac + else + rc=$?; [ $rc -gt 128 ] || { echo "fifo read failed: $rc" >&2; exit 1; } + sample timed; next=$(($(date +%s) + 60)) + fi +done