diff --git a/crates/core/src/qemu.rs b/crates/core/src/qemu.rs index e0be881..f7c211e 100644 --- a/crates/core/src/qemu.rs +++ b/crates/core/src/qemu.rs @@ -6,71 +6,57 @@ use std::path::{Path, PathBuf}; use std::process::Command; -/// A guest arch. Natively it is the host's (`ARCH`): kSTEP never cross-compiles or -/// cross-emulates; the website's QEMU is always aarch64. Every arch-specific fact lives here. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Arch { - X86_64, - Aarch64, -} - +/// The guest arch this build boots. Natively it is the host's: kSTEP never cross-compiles or +/// cross-emulates. The website's wasm build always boots aarch64. #[cfg(target_arch = "x86_64")] -pub const ARCH: Arch = Arch::X86_64; -#[cfg(target_arch = "aarch64")] -pub const ARCH: Arch = Arch::Aarch64; +pub const ARCH: &ArchSpec = &X86_64; +#[cfg(any(target_arch = "aarch64", target_arch = "wasm32"))] +pub const ARCH: &ArchSpec = &AARCH64; -impl Arch { - /// As `uname -m` spells it: the config fragment suffix and the qemu-system binary - pub const fn name(self) -> &'static str { - match self { - Arch::X86_64 => "x86_64", - Arch::Aarch64 => "aarch64", - } - } +/// What differs between the arches, side by side. Read as a table: each field is one fact, the +/// two constants below are its two values. +#[derive(Debug, PartialEq, Eq)] +pub struct ArchSpec { + /// As `uname -m` spells it: the config fragment suffix + pub name: &'static str, + /// The QEMU binary: natively on PATH, and the website's Emscripten build of the same name + pub qemu: &'static str, /// The image QEMU boots, relative to the kernel tree - pub const fn kernel_image(self) -> &'static str { - match self { - Arch::X86_64 => "arch/x86/boot/bzImage", - Arch::Aarch64 => "arch/arm64/boot/Image", - } - } + pub kernel_image: &'static str, /// Guest-physical address of the first byte of RAM: offset 0 of a RAM file - pub const fn ram_base(self) -> u64 { - match self { - Arch::X86_64 => 0, - Arch::Aarch64 => 0x4000_0000, - } - } - /// QEMU's machine type; None for the arch's default (pc) - const fn machine(self) -> Option<&'static str> { - match self { - Arch::X86_64 => None, - Arch::Aarch64 => Some("virt"), - } - } + pub ram_base: u64, + /// QEMU's machine type: named on both arches, so a QEMU changing its default changes nothing here + machine: &'static str, /// The CPU model under TCG (KVM uses `host`) - const fn tcg_cpu(self) -> &'static str { - match self { - Arch::X86_64 => "max", - Arch::Aarch64 => "cortex-a57", - } - } + tcg_cpu: &'static str, /// The virtio-serial transport: PCI on pc, MMIO on virt (no PCI host there) - const fn virtio_serial(self) -> &'static str { - match self { - Arch::X86_64 => "virtio-serial-pci", - Arch::Aarch64 => "virtio-serial-device", - } - } + virtio_serial: &'static str, /// The kernel console on the machine's UART, and what the clock needs under emulation - const fn console_args(self) -> &'static [&'static str] { - match self { - Arch::X86_64 => &["console=ttyS0", "tsc=nowatchdog", "tsc=reliable"], - Arch::Aarch64 => &["console=ttyAMA0"], - } - } + console_args: &'static [&'static str], } +pub const X86_64: ArchSpec = ArchSpec { + name: "x86_64", + qemu: "qemu-system-x86_64", + kernel_image: "arch/x86/boot/bzImage", + ram_base: 0, + machine: "pc", + tcg_cpu: "max", + virtio_serial: "virtio-serial-pci", + console_args: &["console=ttyS0", "tsc=nowatchdog", "tsc=reliable"], +}; + +pub const AARCH64: ArchSpec = ArchSpec { + name: "aarch64", + qemu: "qemu-system-aarch64", + kernel_image: "arch/arm64/boot/Image", + ram_base: 0x4000_0000, + machine: "virt", + tcg_cpu: "cortex-a57", + virtio_serial: "virtio-serial-device", + console_args: &["console=ttyAMA0"], +}; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Machine { pub num_cpus: u32, @@ -111,7 +97,7 @@ fn served(path: &Path) -> String { #[derive(Debug, Clone)] pub struct Boot { /// The guest's arch: the host's natively, aarch64 in the browser - pub arch: Arch, + pub arch: &'static ArchSpec, pub kernel: PathBuf, pub rootfs: PathBuf, pub driver: String, @@ -144,23 +130,21 @@ impl Boot { /// instead of one port I/O exit per byte on a 16550), then where the channels go. QEMU /// creates chardevs before devices, whatever the order. pub fn argv(&self) -> Vec { - let mut machine: Vec<&str> = self.arch.machine().into_iter().collect(); - if self.ram_file.is_some() { - machine.push("memory-backend=ram"); - } + // Guest RAM as a file the host can read the shared region out of: the backend object, and + // the machine told to use it. The browser has neither and reads the wasm heap directly. let mut argv: Vec = vec![]; - if !machine.is_empty() { - argv.extend(["-machine".into(), machine.join(",")]); - } - if let Some(ram) = &self.ram_file { - let m = self.machine.mem_mb; - argv.extend([ + match &self.ram_file { + Some(ram) => argv.extend([ + "-machine".into(), + format!("{},memory-backend=ram", self.arch.machine), "-object".into(), format!( - "memory-backend-file,id=ram,size={m}M,mem-path={},share=on", + "memory-backend-file,id=ram,size={}M,mem-path={},share=on", + self.machine.mem_mb, ram.display() ), - ]); + ]), + None => argv.extend(["-machine".into(), self.arch.machine.into()]), } // KVM where the host offers it, else TCG: one host thread per vCPU (MTTCG) and a 64 MB // translation cache, enough for kSTEP's small kernels and what fits next to guest RAM in @@ -168,7 +152,7 @@ impl Boot { let (accel, cpu) = if kvm_available() { ("kvm", "host") } else { - ("tcg,tb-size=64,thread=multi", self.arch.tcg_cpu()) + ("tcg,tb-size=64,thread=multi", self.arch.tcg_cpu) }; argv.extend( [ @@ -204,7 +188,7 @@ impl Boot { "-serial", "chardev:console", "-device", - &format!("{},id=vs0", self.arch.virtio_serial()), + &format!("{},id=vs0", self.arch.virtio_serial), "-device", "virtconsole,bus=vs0.0,nr=0,chardev=port", ] @@ -275,14 +259,14 @@ impl Boot { "init=/user".into(), "panic=-1".into(), // exit immediately on panic ]; - args.extend(self.arch.console_args().iter().map(|s| s.to_string())); + args.extend(self.arch.console_args.iter().map(|s| s.to_string())); args.push("--".into()); args.push(format!("driver={}", self.driver)); args.join(" ") } pub fn command(&self) -> Command { - let mut c = Command::new(format!("qemu-system-{}", self.arch.name())); + let mut c = Command::new(self.arch.qemu); c.args(self.argv()); c } @@ -295,7 +279,7 @@ mod tests { /// The page's boot, as wasm.rs makes it fn browser(smp: u32) -> Boot { Boot { - arch: Arch::Aarch64, + arch: &AARCH64, kernel: "/kernel".into(), rootfs: "/rootfs.cpio".into(), driver: "cli".into(), @@ -320,7 +304,7 @@ mod tests { let c = boot.cmdline(); assert!(c.contains("isolcpus=nohz,managed_irq,1-2 "), "{c}"); assert!(c.ends_with("console=ttyAMA0 -- driver=cli"), "{c}"); - boot.arch = Arch::X86_64; + boot.arch = &X86_64; boot.machine.num_cpus = 2; boot.driver = "default".into(); let x = boot.cmdline(); @@ -348,7 +332,7 @@ mod tests { #[test] fn native_boot_wires_the_channels() { let mut boot = Boot { - arch: Arch::X86_64, + arch: &X86_64, kernel: "/b/kernel".into(), rootfs: "/b/rootfs.cpio".into(), driver: "cli".into(), diff --git a/crates/core/src/shm.rs b/crates/core/src/shm.rs index e98b449..220a65b 100644 --- a/crates/core/src/shm.rs +++ b/crates/core/src/shm.rs @@ -1,10 +1,9 @@ //! kmod/shm.h, byte for byte: the region of guest memory the cli driver rewrites after every //! command. The structs come from the header itself, through bindgen (build.rs), as `raw`, so a -//! snapshot of the region is read as one `kstep_shm` and each table sliced by its count. `gen` is -//! odd while the writer is mid-update; a snapshot with an odd gen is `Busy`. That is the whole -//! check: the kmod rewrites the region before it replies, and a host snapshots after the reply, -//! so a snapshot never straddles an update. A host that read live memory concurrently would have -//! to compare gen before and after its copy itself. +//! snapshot of the region is read as one `kstep_shm` and each table sliced by its count. The kmod +//! rewrites the region before it replies and a host snapshots after the reply, so a snapshot never +//! straddles an update; the one check is the layout number, against a region written by another +//! revision of the header. use serde::Serialize; @@ -15,7 +14,6 @@ pub mod raw { } use raw::*; -pub const MAGIC: u32 = KSTEP_SHM_MAGIC; pub const LAYOUT: u32 = KSTEP_SHM_LAYOUT; /// The whole region: what to snapshot pub const SIZE: usize = std::mem::size_of::(); @@ -24,12 +22,10 @@ pub const COV_SIZE: usize = KSTEP_COV_SIZE as usize; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Error { - /// Not a kSTEP region - Magic(u32), /// A region this decoder was not built for: rebuild the image from the current kmod Layout(u32), - /// Mid-update: read again - Busy, + /// A table's count exceeds its capacity: not a region the kmod wrote + Count, /// The snapshot is shorter than the region Short, } @@ -37,9 +33,8 @@ pub enum Error { impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - Error::Magic(m) => write!(f, "not a kSTEP shared region (magic {m:#x})"), Error::Layout(l) => write!(f, "shm layout {l}, this decoder speaks {LAYOUT}: rebuild the image from the current kmod"), - Error::Busy => f.write_str("shm mid-update"), + Error::Count => f.write_str("shm table count over its capacity"), Error::Short => write!(f, "shm snapshot shorter than the region ({SIZE} bytes)"), } } @@ -62,6 +57,8 @@ fn cstr(s: &[std::os::raw::c_char]) -> String { pub struct State { /// Logical ticks pub timestamp: u32, + /// The fair class is EEVDF (6.6+): entities carry lag, deadline, eligibility and the pick + pub eevdf: bool, pub cpus: Vec, pub tasks: Vec, /// The cgroups, root first @@ -218,15 +215,14 @@ fn policy(n: u32) -> &'static str { } } -/// The first `n` records of a table, or `Busy` when a half-written header claims more than fit. +/// The first `n` records of a table, or `Count` when the header claims more than fit. fn table(rows: &[R], n: u32, f: impl Fn(&R) -> T) -> Result, Error> { rows.get(..n as usize) - .ok_or(Error::Busy) + .ok_or(Error::Count) .map(|rows| rows.iter().map(f).collect()) } -/// Decode one snapshot of the region (`SIZE` bytes). `Busy` means the writer was mid-update: -/// snapshot again. +/// Decode one snapshot of the region (`SIZE` bytes). pub fn decode(b: &[u8]) -> Result { if b.len() < SIZE { return Err(Error::Short); @@ -235,15 +231,9 @@ pub fn decode(b: &[u8]) -> Result { // valid value; read unaligned from a checked range. let r: kstep_shm = unsafe { std::ptr::read_unaligned(b.as_ptr() as *const kstep_shm) }; let h = &r.hdr; - if h.magic != MAGIC { - return Err(Error::Magic(h.magic)); - } if h.layout != LAYOUT { return Err(Error::Layout(h.layout)); } - if h.gen_ & 1 == 1 { - return Err(Error::Busy); - } let cpus = table(&r.cpu, h.ncpus, |c| Cpu { cpu: c.cpu, @@ -318,30 +308,41 @@ pub fn decode(b: &[u8]) -> Result { position: e.position, time_slice: e.time_slice, })?; + // a domain's flags word, as the names the kernel wrote for its bits, "A, B, C" + let flag_names = |mask: u64| { + (0..r.sd_flag.len()) + .filter(|&bit| mask >> bit & 1 == 1) + .map(|bit| cstr(&r.sd_flag[bit])) + .filter(|n| !n.is_empty()) + .collect::>() + .join(", ") + }; + let all_groups = table(&r.group, h.ngroups, |g| Group { + span: g.span, + capacity: g.capacity, + min_capacity: g.min_capacity, + max_capacity: g.max_capacity, + weight: g.weight, + })?; let domains = table(&r.domain, h.ndomains, |d| Domain { cpu: d.cpu, span: d.span, name: cstr(&d.name), - flags: cstr(&d.flags), + flags: flag_names(d.flags), imbalance_pct: d.imbalance_pct, balance_interval: d.balance_interval, busy_factor: d.busy_factor, cache_nice_tries: d.cache_nice_tries, nr_balance_failed: d.nr_balance_failed, last_balance_ago: d.last_balance_ago, - groups: d.group[..(d.ngroups as usize).min(d.group.len())] - .iter() - .map(|g| Group { - span: g.span, - capacity: g.capacity, - min_capacity: g.min_capacity, - max_capacity: g.max_capacity, - weight: g.weight, - }) - .collect(), + groups: all_groups + .get(d.group as usize..(d.group + d.ngroups) as usize) + .map(<[Group]>::to_vec) + .unwrap_or_default(), })?; Ok(State { timestamp: h.timestamp, + eevdf: h.features & KSTEP_SHM_EEVDF != 0, cpus, tasks, groups, @@ -367,14 +368,14 @@ mod tests { fn decodes_a_region() { let mut r = kstep_shm { hdr: kstep_shm_hdr { - magic: MAGIC, layout: LAYOUT, - gen_: 2, + features: KSTEP_SHM_EEVDF, timestamp: 7, ncpus: 1, ntasks: 1, ncgroups: 1, - ndomains: 0, + ndomains: 1, + ngroups: 2, nentities: 1, nrt_entities: 1, }, @@ -427,9 +428,24 @@ mod tests { time_slice: 99, ..Default::default() }; + // one domain with two groups, and flag bits 0 and 2 named by the kernel + r.sd_flag[0][..3].copy_from_slice(&[b'S' as _, b'M' as _, b'T' as _]); + r.sd_flag[2][..3].copy_from_slice(&[b'L' as _, b'L' as _, b'C' as _]); + r.domain[0] = kstep_shm_domain { + cpu: 1, + span: 0b110, + flags: 0b101, + group: 0, + ngroups: 2, + ..Default::default() + }; + r.domain[0].name[..2].copy_from_slice(&[b'M' as _, b'C' as _]); + r.group[0].span = 0b010; + r.group[1].span = 0b100; let st = decode(&bytes(&r)).unwrap(); assert_eq!(st.timestamp, 7); + assert!(st.eevdf); assert_eq!( st.cpus, [Cpu { @@ -482,13 +498,15 @@ mod tests { ), (100, true, 99) ); - assert!(st.domains.is_empty()); + let d = &st.domains[0]; + assert_eq!((d.name.as_str(), d.flags.as_str()), ("MC", "SMT, LLC")); + assert_eq!( + d.groups.iter().map(|g| g.span).collect::>(), + [0b010, 0b100] + ); - r.hdr.gen_ = 3; - assert_eq!(decode(&bytes(&r)), Err(Error::Busy)); - r.hdr.gen_ = 2; r.hdr.ntasks = KSTEP_SHM_TASKS + 1; - assert_eq!(decode(&bytes(&r)), Err(Error::Busy)); + assert_eq!(decode(&bytes(&r)), Err(Error::Count)); r.hdr.layout = LAYOUT + 1; assert_eq!(decode(&bytes(&r)), Err(Error::Layout(LAYOUT + 1))); assert_eq!(decode(&bytes(&r)[..100]), Err(Error::Short)); diff --git a/crates/core/src/wasm.rs b/crates/core/src/wasm.rs index d402cf0..2f1b2a6 100644 --- a/crates/core/src/wasm.rs +++ b/crates/core/src/wasm.rs @@ -4,7 +4,7 @@ use wasm_bindgen::prelude::*; -use crate::qemu::{Arch, Boot, Machine}; +use crate::qemu::{Boot, Machine, ARCH}; use crate::shm; /// The argv for the page's QEMU (an aarch64 build): the cli driver on `smp` CPUs with `mem` MB, @@ -13,7 +13,7 @@ use crate::shm; #[wasm_bindgen] pub fn qemu_args(smp: u32, mem: u32, snapshot: bool) -> Vec { Boot { - arch: Arch::Aarch64, + arch: ARCH, snapshot: snapshot.then(|| "/snap".into()), kernel: "/kernel".into(), rootfs: "/rootfs.cpio".into(), @@ -40,14 +40,13 @@ pub fn shm_size() -> usize { shm::SIZE } -/// The machine's state from a snapshot of the region; `null` when the writer was mid-update. -/// Fails on a region this decoder was not built for. Every field fits a JS number: the kernel's +/// The machine's state from a snapshot of the region. Fails on a region this decoder was not +/// built for. Every field fits a JS number: the kernel's /// wrapping vruntimes are decoded signed, and nothing else gets near 2^53. #[wasm_bindgen] pub fn shm_decode(region: &[u8]) -> Result { match shm::decode(region) { Ok(state) => serde_wasm_bindgen::to_value(&state).map_err(|e| JsError::new(&e.to_string())), - Err(shm::Error::Busy) => Ok(JsValue::NULL), Err(e) => Err(JsError::new(&e.to_string())), } } diff --git a/crates/kstep/src/build.rs b/crates/kstep/src/build.rs index abd6f7e..cd209af 100644 --- a/crates/kstep/src/build.rs +++ b/crates/kstep/src/build.rs @@ -175,7 +175,7 @@ impl Build { let linux = proj_dir().join("linux"); let mut fragments = vec![ linux.join("config.kstep"), - linux.join(format!("config.kstep.{}", ARCH.name())), + linux.join(format!("config.kstep.{}", ARCH.name)), ]; let bug_config = crate::bugs::for_build(&self.name)? .and_then(|b| b.config) @@ -212,7 +212,7 @@ impl Build { "all", "compile_commands.json", ]); - let image = self.linux().join(ARCH.kernel_image()); + let image = self.linux().join(ARCH.kernel_image); let before = mtime(&image); run(&mut make, log)?; // fs::copy gives a fresh mtime, which kernel_stale compares with the .config's diff --git a/crates/kstep/src/cli/viz.rs b/crates/kstep/src/cli/viz.rs index f1c407d..69a1a84 100644 --- a/crates/kstep/src/cli/viz.rs +++ b/crates/kstep/src/cli/viz.rs @@ -1,8 +1,6 @@ //! `kstep viz`: build the visualization website (website/site), serve it, or publish it. use std::fs; -use std::io::{BufRead, BufReader, Write}; -use std::net::TcpListener; use std::path::PathBuf; use anyhow::{bail, Context, Result}; @@ -10,36 +8,40 @@ use clap::Subcommand; use kstep::bugs::{self, Bug}; use kstep::cmd::{cmd, output, run}; use kstep::Build; -use kstep_core::qemu::{Arch, ARCH}; +use kstep_core::qemu::{AARCH64, ARCH}; use serde_json::json; -/// Serve the website at http://localhost:PORT/ (built first if it never was); `build` and `deploy` +/// Build the website and serve it at http://localhost:PORT/; `build`, `serve` and `deploy` do one thing each #[derive(clap::Args)] #[command( after_help = "The site is website/site: the tracked page plus qemu/ (from website/setup.sh) and what `build` -writes there: data.json, images/cli (the playground image, kSTEP's build/v6.18, and its snapshot) -and kstep_core.js + kstep_core_bg.wasm (crates/core for wasm32). Edits to the page show on a -reload; `build` again after changing the kmod, user.c, crates/core or bugs.yaml." +writes there: data.json, images// (a playground image per supported LTS kernel, from +kSTEP's build/, and its snapshot) and kstep_core.js + kstep_core_bg.wasm (crates/core for +wasm32). Plain `kstep viz` builds first, incrementally (an unchanged image keeps its snapshot), then +serves; `serve` skips the build, for page edits, which show on a reload." )] pub struct Args { #[command(subcommand)] cmd: Option, - #[arg(long, default_value_t = 8080)] + #[arg(long, global = true, default_value_t = 8080)] port: u16, } #[derive(Subcommand)] enum Cmd { - /// Rebuild the bug catalog, the wasm decoder, the playground image and its snapshot + /// Rebuild the bug catalog, the wasm decoder, the playground images and their snapshots Build, + /// Serve site/ as it is, without building + Serve, /// Build, gate on pagetest.mjs and `run.mjs --check`, force-push site/ as gh-pages Deploy, } const KSTEP_URL: &str = "https://github.com/kstep-dev/kstep/blob/master"; const RESULTS_URL: &str = "https://raw.githubusercontent.com/kstep-dev/results/main"; -/// The playground image: Linux v6.18 for arm64, shared with the repro builds -const IMAGE: &str = "v6.18"; +/// The playground images: every supported LTS kernel for arm64, shared with the repro builds; +/// the page boots this one unless the URL says otherwise +const DEFAULT_IMAGE: &str = "v6.18"; fn website() -> PathBuf { kstep::proj_dir().join("website") @@ -67,6 +69,15 @@ fn node() -> Result { .context("no node: run website/setup.sh or install Node >= 20") } +/// One of website/'s scripts under node, from website/. +fn mjs(node: &std::path::Path, script: &str, args: &[&str]) -> std::process::Command { + let mut c = cmd(node.to_str().unwrap(), ["--wasm-lazy-compilation"]); + c.arg(website().join(script)) + .args(args) + .current_dir(website()); + c +} + /// What the bug catalog shows (the README's results table says the same by hand). fn catalog(bug: &Bug) -> serde_json::Value { let commit = |h: &str| json!({ "label": format!("linux@{}", &h[..7]), "url": format!("https://github.com/torvalds/linux/commit/{h}") }); @@ -116,14 +127,6 @@ fn build_decoder() -> Result<()> { ) .context("the wasm target: rustup target add wasm32-unknown-unknown")?; let wasm = root.join("target/wasm32-unknown-unknown/wasm/kstep_core.wasm"); - // the CLI must match the wasm-bindgen crate in Cargo.lock exactly - let lock = fs::read_to_string(root.join("Cargo.lock"))?; - let version = lock - .split("name = \"wasm-bindgen\"\n") - .nth(1) - .and_then(|s| s.lines().next()) - .map(|l| l.trim_start_matches("version = ").trim_matches('"')) - .unwrap_or("?"); run( cmd( "wasm-bindgen", @@ -133,14 +136,14 @@ fn build_decoder() -> Result<()> { .arg(&wasm), None, ) - .with_context(|| format!("wasm-bindgen: cargo install wasm-bindgen-cli --version {version}")) + .context("wasm-bindgen: cargo install wasm-bindgen-cli at the version in Cargo.lock") } fn build() -> Result { - if !site().join("qemu/qemu-system-aarch64.wasm").exists() { + if !site().join(format!("qemu/{}.wasm", AARCH64.qemu)).exists() { bail!("no wasm QEMU in website/site/qemu; run website/setup.sh"); } - if ARCH != Arch::Aarch64 { + if *ARCH != AARCH64 { bail!("the playground image is arm64; build the site on an arm64 host"); } build_decoder()?; @@ -156,30 +159,45 @@ fn build() -> Result { let bugs: Vec<_> = sorted.into_iter().map(catalog).collect(); fs::write( site().join("data.json"), - serde_json::to_string_pretty(&json!({ "version": version, "bugs": bugs }))?, + serde_json::to_string_pretty(&json!({ + "version": version, "bugs": bugs, + "kernels": kstep::checkout::LTS, "kernel": DEFAULT_IMAGE, + }))?, )?; - // The playground image, rebuilt from the current kmod and user.c so the page and the driver - // it talks to are always published together. First time: a kernel build (~10 min). - let b = Build::new(Some(IMAGE))?; - b.build_kstep()?; - let images = site().join("images/cli"); - fs::create_dir_all(&images)?; - fs::copy(b.kernel(), images.join("kernel"))?; - fs::copy(b.rootfs(), images.join("rootfs.cpio"))?; - // The machine at the driver's ready line for the page's default CPU count, so the page resumes - // it instead of booting (~0.3 s instead of ~4 s). The stream fits this QEMU build and image - // only, hence next to the image and regenerated with it. Needs site/qemu (website/setup.sh). + // The playground images, one per supported LTS kernel, rebuilt from the current kmod and + // user.c so the page and the driver it talks to are always published together. First time: + // a kernel build each (~10 min). The machine at the driver's ready line for the page's + // default CPU count goes next to each, so the page resumes it instead of booting (~0.3 s + // instead of ~4 s); the stream fits this QEMU build and image only, so it is taken again + // exactly when the image changed. Needs site/qemu (website/setup.sh). let node = node()?; - run( - cmd(node.to_str().unwrap(), ["--wasm-lazy-compilation"]) - .arg(website().join("run.mjs")) - .args(["--snapshot", "--image"]) - .arg(&images) - .current_dir(website()), - None, - ) - .context("snapshot of the playground image (website/run.mjs --snapshot)")?; + for kernel in kstep::checkout::LTS { + let b = Build::new(Some(kernel))?; + b.build_kstep()?; + let images = site().join("images").join(kernel); + fs::create_dir_all(&images)?; + let same = |src: PathBuf, dst: &str| fs::read(&src).ok() == fs::read(images.join(dst)).ok(); + if same(b.kernel(), "kernel") + && same(b.rootfs(), "rootfs.cpio") + && images.join("snap-5.json").is_file() + { + continue; + } + fs::copy(b.kernel(), images.join("kernel"))?; + fs::copy(b.rootfs(), images.join("rootfs.cpio"))?; + run( + &mut mjs( + &node, + "run.mjs", + &["--snapshot", "--image", images.to_str().unwrap()], + ), + None, + ) + .with_context(|| { + format!("snapshot of the {kernel} playground image (website/run.mjs --snapshot)") + })?; + } let size = output(cmd("du", ["-sh"]).arg(site()))? .split_whitespace() .next() @@ -189,126 +207,41 @@ fn build() -> Result { Ok(version) } -/// A static file server for site/, for a browser possibly far away (a VS Code port forward): -/// keep-alive, so one connection carries the page's ~40 requests, and an ETag from the file's -/// size and mtime, so a refresh gets 304s instead of the 14 MB QEMU again. Everything the page -/// fetches is versioned (?v=) except index.html, so no-cache: the browser revalidates every -/// time and sees an edit at once. +/// Python's static file server on site/, bound to localhost. It answers conditional requests with +/// 304s, so a refresh does not fetch the 14 MB QEMU again, and a browser revalidates the document +/// itself on every reload, so an edit to index.html shows at once. No keep-alive (HTTP/1.0): the +/// page's ~40 requests each open a connection, which over a local port forward is not felt. fn serve(port: u16) -> Result<()> { - let listener = - TcpListener::bind(("127.0.0.1", port)).with_context(|| format!("bind port {port}"))?; println!("http://localhost:{port}/"); - for stream in listener.incoming().flatten() { - std::thread::spawn(move || { - let _ = connection(stream); - }); - } - Ok(()) -} - -fn connection(mut stream: std::net::TcpStream) -> std::io::Result<()> { - let mut reader = BufReader::new(stream.try_clone()?); - loop { - let t0 = std::time::Instant::now(); - let mut req = String::new(); - if reader.read_line(&mut req)? == 0 { - return Ok(()); // the browser closed the connection - } - let mut etag_seen = None; - let mut close = false; - let mut line = String::new(); - loop { - line.clear(); - if reader.read_line(&mut line)? == 0 || line == "\r\n" || line == "\n" { - break; // the blank line ends the headers - } - if let Some((name, value)) = line.split_once(':') { - match name.to_ascii_lowercase().as_str() { - "if-none-match" => etag_seen = Some(value.trim().to_string()), - "connection" if value.trim().eq_ignore_ascii_case("close") => close = true, - _ => {} - } - } - } - let path = req - .split_whitespace() - .nth(1) - .unwrap_or("/") - .split('?') - .next() - .unwrap_or("/") - .to_string(); - let rel = path.trim_start_matches('/'); - let file = site().join(if rel.is_empty() { "index.html" } else { rel }); - let mime = match file.extension().and_then(|e| e.to_str()) { - Some("html") => "text/html", - Some("js") | Some("mjs") => "text/javascript", - Some("css") => "text/css", - Some("json") => "application/json", - Some("wasm") => "application/wasm", - Some("png") => "image/png", - Some("svg") => "image/svg+xml", - Some("pdf") => "application/pdf", - Some("gz") => "application/gzip", // the snapshot: raw bytes, the page gunzips them itself - _ => "application/octet-stream", - }; - let meta = fs::metadata(&file) - .ok() - .filter(|m| m.is_file() && !rel.contains("..")); - let etag = meta.as_ref().map(|m| { - let mtime = m - .modified() - .ok() - .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) - .map_or(0, |d| d.as_nanos()); - format!("\"{}-{mtime:x}\"", m.len()) - }); - let (status, bytes) = match (&etag, etag_seen) { - (Some(tag), Some(seen)) if *tag == seen => { - write!( - stream, - "HTTP/1.1 304 Not Modified\r\nETag: {tag}\r\nCache-Control: no-cache\r\n\r\n" - )?; - (304, 0) - } - (Some(tag), _) => { - let body = fs::read(&file)?; - write!(stream, "HTTP/1.1 200 OK\r\nContent-Type: {mime}\r\nContent-Length: {}\r\nETag: {tag}\r\nCache-Control: no-cache\r\n\r\n", body.len())?; - stream.write_all(&body)?; - (200, body.len()) - } - (None, _) => { - write!( - stream, - "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n" - )?; - (404, 0) - } - }; - // one line per request: what the browser asked for and how long it took - eprintln!("{status} {path} {bytes} B {} ms", t0.elapsed().as_millis()); - if close { - return Ok(()); - } - } + run( + cmd("python3", ["-m", "http.server", "-b", "127.0.0.1", "-d"]) + .arg(site()) + .arg(port.to_string()), + None, + ) + .context("python3 -m http.server") } -/// Refuse to publish a page whose playground image does not speak its protocol, then push site/ +/// Refuse to publish a page whose playground images do not speak its protocol, then push site/ /// as the orphan gh-pages branch. fn deploy(version: &str) -> Result<()> { let node = node()?; - run( - cmd(node.to_str().unwrap(), [website().join("pagetest.mjs")]).current_dir(website()), - None, - ) - .context("deploy aborted: site/viz.mjs failed its checks")?; - run( - cmd(node.to_str().unwrap(), ["--wasm-lazy-compilation"]) - .arg(website().join("run.mjs")) - .arg("--check"), - None, - ) - .context("deploy aborted: rebuild the playground image from the current kmod and user.c")?; + run(&mut mjs(&node, "pagetest.mjs", &[]), None) + .context("deploy aborted: site/viz.mjs failed its checks")?; + for kernel in kstep::checkout::LTS { + let image = site().join("images").join(kernel); + run( + &mut mjs( + &node, + "run.mjs", + &["--check", "--image", image.to_str().unwrap()], + ), + None, + ) + .with_context(|| { + format!("deploy aborted: the {kernel} playground image failed its check") + })?; + } let origin = output(cmd("git", ["remote", "get-url", "origin"]).current_dir(website()))? .trim() .to_string(); @@ -340,11 +273,10 @@ fn deploy(version: &str) -> Result<()> { pub fn main(a: Args) -> Result<()> { match a.cmd { Some(Cmd::Build) => build().map(|_| ()), + Some(Cmd::Serve) => serve(a.port), Some(Cmd::Deploy) => deploy(&build()?), None => { - if !site().join("data.json").exists() { - build()?; - } + build()?; // incremental: what is current stays as it is serve(a.port) } } diff --git a/crates/kstep/src/session.rs b/crates/kstep/src/session.rs index 7e89c45..6a8eb82 100644 --- a/crates/kstep/src/session.rs +++ b/crates/kstep/src/session.rs @@ -13,7 +13,7 @@ use std::path::{Path, PathBuf}; use std::process::{Child, Stdio}; use std::time::{Duration, Instant}; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{bail, Context, Result}; use kstep_core::qemu::{Boot, ARCH}; use kstep_core::shm::{self, State}; use serde_json::Value; @@ -119,14 +119,14 @@ impl Session { .and_then(Value::as_u64) .context("ready line without shm address")?; let shm_at = shm - .checked_sub(ARCH.ram_base()) + .checked_sub(ARCH.ram_base) .context("shm address below RAM")?; // 0 where the kernel has no coverage map (built without linux/config.kstep.cov) let cov_at = ready .get("cov") .and_then(Value::as_u64) .filter(|&c| c != 0) - .and_then(|c| c.checked_sub(ARCH.ram_base())); + .and_then(|c| c.checked_sub(ARCH.ram_base)); let mut s = Session { child, reader, @@ -165,19 +165,13 @@ impl Session { read_reply(&mut self.reader) } - /// The machine's state as of the last reply. The kmod writes the region before it replies, - /// so a snapshot taken now is consistent; a `Busy` snapshot is retried a few times anyway. + /// The machine's state as of the last reply: the kmod writes the region before it replies, + /// so a snapshot taken now is consistent. pub fn state(&mut self) -> Result { - for _ in 0..100 { - self.ram - .read_exact_at(&mut self.snapshot, self.shm_at) - .context("read shm")?; - match shm::decode(&self.snapshot) { - Err(shm::Error::Busy) => std::thread::sleep(Duration::from_millis(1)), - r => return r.map_err(Into::into), - } - } - Err(anyhow!("shm stayed mid-update")) + self.ram + .read_exact_at(&mut self.snapshot, self.shm_at) + .context("read shm")?; + shm::decode(&self.snapshot).map_err(Into::into) } /// Copy the guest's coverage map out (`map` is `shm::COV_SIZE` bytes); false when this diff --git a/kmod/cli.c b/kmod/cli.c index 448f68d..fcaedca 100644 --- a/kmod/cli.c +++ b/kmod/cli.c @@ -23,7 +23,7 @@ // cgroup-cpus /a cpuset.cpus // cgroup-destroy /a it must hold no task and no child // cgroup-attach /a `cgroup-attach / ` moves the task back to the root -// cpu-topo KEY=g|g;... e.g. CLS=1-2|3-4;CAP=2,4:512: groups per level and CAP groups +// cpu-topo KEY=g|g;... e.g. CPUS=4;CLS=1-2|3-4;CAP=2,4:512: the test CPU count, groups per level and CAP groups // cpulist:scale; never CPU 0; rebuilds the sched domains (cpu.c) // cpu-freq current frequency, as cpufreq changes it under a running system // check enable a checker (checkers/); a rule that fires emits a warn record @@ -110,7 +110,7 @@ static void cmd_tick(char *arg) { } static void cmd_affinity(char *arg) { - const char *usage = "usage: affinity "; + const char *usage = "usage: affinity "; struct task_struct *p = parse_task(&arg, usage); if (!p) @@ -230,7 +230,7 @@ static void cmd_cgroup_weight(char *arg) { } static void cmd_cgroup_cpus(char *arg) { - const char *usage = "usage: cgroup-cpus /path "; + const char *usage = "usage: cgroup-cpus /path "; const char *name = parse_cgroup(&arg, usage, false); struct cpumask mask; @@ -288,7 +288,7 @@ static void cmd_cpu_scale(char *arg, const char *usage, void (*set)(int cpu, int if (!val) return reply_error(usage); *val++ = '\0'; - if (kstrtoint(arg, 10, &cpu) || cpu < 1 || cpu >= num_online_cpus() || kstrtoint(val, 10, &scale) || + if (kstrtoint(arg, 10, &cpu) || cpu < 1 || cpu > kstep_test_ncpus || kstrtoint(val, 10, &scale) || scale < 1 || scale > SCHED_CAPACITY_SCALE) return reply_error(usage); set(cpu, scale); @@ -300,7 +300,7 @@ static void cmd_cpu_freq(char *arg) { static void cmd_cpu_topo(char *arg) { if (!arg) - return reply_error("usage: cpu-topo KEY=group|group[;...] (levels and CAP, see cpu.c)"); + return reply_error("usage: cpu-topo KEY=group|group[;...] (levels, CAP and CPUS=K, see cpu.c)"); const char *err = kstep_topo_set(arg); if (err) reply_error(err); diff --git a/kmod/cpu.c b/kmod/cpu.c index a9a272d..14c37b5 100644 --- a/kmod/cpu.c +++ b/kmod/cpu.c @@ -50,10 +50,9 @@ static const struct cpumask *topo_mask(struct sched_domain_topology_level *tl, static void print_topo_levels(void) { pr_info("Topology levels:\n"); struct sched_domain_topology_level *tl; - int nr_cpus = num_online_cpus(); for_each_tl(tl) { pr_info("- %-5s| ", tl->name); - for (int cpu = 1; cpu < nr_cpus; cpu++) { + for_each_test_cpu(cpu) { const struct cpumask *mask = topo_mask(tl, cpu); print_cpumask(mask, 4); pr_cont(" | "); @@ -81,9 +80,8 @@ static void print_sched_domain(struct sched_domain *sd) { static void print_sched_domains(void) { pr_info("Sched domains:\n"); struct sched_domain_topology_level *tl; - int nr_cpus = num_online_cpus(); for_each_tl(tl) { - for (int cpu = 1; cpu < nr_cpus; cpu++) { + for_each_test_cpu(cpu) { struct sched_domain *sd; for_each_domain(cpu, sd) { if (strcmp(sd->name, tl->name) != 0) @@ -139,18 +137,41 @@ static int get_topo_level(const char *name) { return strcmp(name, "DIE") == 0 ? KSTEP_TOPO_PKG : -1; } -// The machine's own masks, with CPU 0 alone at every level so the test CPUs' load balancers never -// read its non-deterministic state; and the kernel's topology table pointed at our masks. +int kstep_test_ncpus; + +// The CPUs the module works with: as many as it has room for, every one but the controller's a +// test CPU, on the machine's own topology with CPU 0 isolated -- until a driver's cpu-topo spec +// says otherwise. +void kstep_cpu_init(void) { + if (num_online_cpus() > KSTEP_NR_CPUS) + panic("Number of online CPUs (%d) exceeds KSTEP_NR_CPUS (%d)", num_online_cpus(), KSTEP_NR_CPUS); + kstep_test_ncpus = num_online_cpus() - 1; + kstep_topo_set(""); +} + +// The machine's own masks over the test CPUs, with CPU 0 and every spare CPU alone at every level, +// so the test CPUs' load balancers never read the controller's non-deterministic state and never +// see a spare: a single-CPU level degenerates, leaving them on a null domain, like an offline CPU +// would be without the hotplug the mocked clock cannot do. The kernel's topology table is pointed +// at our masks. static void topo_init(void) { struct sched_domain_topology_level *tl; + struct cpumask test; + + cpumask_clear(&test); + for_each_test_cpu(cpu) + cpumask_set_cpu(cpu, &test); for_each_tl(tl) { int level = get_topo_level(tl->name); if (level < 0) panic("Unknown topology level %s", tl->name); for (int cpu = 0; cpu < num_online_cpus(); cpu++) { - cpumask_copy(&kstep_masks[level][cpu], cpu ? topo_mask(tl, cpu) : cpumask_of(0)); - if (cpu) - cpumask_clear_cpu(0, &kstep_masks[level][cpu]); + struct cpumask *m = &kstep_masks[level][cpu]; + if (!cpumask_test_cpu(cpu, &test)) { + cpumask_copy(m, cpumask_of(cpu)); + continue; + } + cpumask_and(m, topo_mask(tl, cpu), &test); } tl->mask = kstep_masks_fns[level]; } @@ -161,14 +182,16 @@ static void topo_init(void) { // // KEY=group|group;KEY=group|group... a group is a cpulist; CAP groups are cpulist:scale // -// A key is a level (SMT, CLS, MC, PKG, NODE) or CAP. At a level named, the CPUs of a group share -// it and a CPU not named is alone; a level not named keeps the machine's own, and one this kernel -// does not have goes unused. Lower levels must nest in higher ones: the kernel says so itself +// A key is a level (SMT, CLS, MC, PKG, NODE), CAP, or CPUS. CPUS=K makes 1..K the test CPUs (all +// of 1..N-1 when absent); the rest of the machine is spare, idle and unseen, so one boot serves +// every smaller machine. At a level named, the CPUs of a group share it and a CPU not named is +// alone; a level not named keeps the machine's own, and one this kernel does not have goes unused. Lower levels must nest in higher ones: the kernel says so itself // ("arch topology borken") and widens the parent. CAP is the capacity, 1..1024, of the CPUs // named, 1024 for the rest. CPU 0 is the controller's, is never named and stays alone at every // level. // // CLS=1-2|3-4;CAP=2,4:512 two clusters, a little core in each (even_idle_cpu) +// CPUS=2 CPUs 1 and 2 of whatever the machine has; the others idle // Capacity is domain-building state as well as a per-CPU value: SD_ASYM_CPUCAPACITY, the // sched_asym_cpucapacity key and rd->max_cpu_capacity are computed only when the domains are @@ -196,16 +219,16 @@ static void set_cap(int cpu, int scale) { #endif } -// A nonempty cpulist ("1", "1-3", "1,3") of test CPUs: within 1..N-1, never CPU 0, the controller's. +// A nonempty cpulist ("1", "1-3", "1,3") of test CPUs: within 1..K, never CPU 0, the controller's. bool kstep_parse_cpus(const char *list, struct cpumask *mask) { return list && !cpulist_parse(list, mask) && !cpumask_empty(mask) && !cpumask_test_cpu(0, mask) && - cpumask_last(mask) < num_online_cpus(); + cpumask_last(mask) <= kstep_test_ncpus; } // One group's cpulist: test CPUs, each named once per key. static const char *topo_parse_group(const char *list, struct cpumask *group, struct cpumask *seen) { if (!kstep_parse_cpus(list, group)) - return "bad cpulist: test CPUs 1..N-1 only"; + return "bad cpulist: test CPUs 1..CPUS only"; if (cpumask_intersects(group, seen)) return "a CPU is named twice under one key"; cpumask_or(seen, seen, group); @@ -219,10 +242,12 @@ static const char *topo_set_key(const char *key, char *groups, int *caps) { struct cpumask seen, group; char *list; + if (strcmp(key, "CPUS") == 0) + return NULL; // read ahead of the levels, by kstep_topo_cpus if (level < 0) - return "unknown key (a level or CAP)"; + return "unknown key (a level, CAP or CPUS)"; if (!cap) // alone unless named - for (cpu = 1; cpu < num_online_cpus(); cpu++) + for_each_test_cpu(cpu) cpumask_copy(&kstep_masks[level][cpu], cpumask_of(cpu)); cpumask_clear(&seen); while ((list = strsep(&groups, "|")) != NULL) { @@ -246,14 +271,68 @@ static const char *topo_set_key(const char *key, char *groups, int *caps) { return NULL; } +// The test CPU count a spec asks for: CPUS=K within 1..N-1, or N-1 when it says nothing; -1 for +// a malformed value. Read ahead of the rest of the spec, since the levels are bounded by it. +static int topo_cpus(const char *spec) { + char buf[CPU_SPEC_LEN]; + char *cursor = buf, *entry; + int n = num_online_cpus() - 1; + + strscpy(buf, spec, sizeof(buf)); + while ((entry = strsep(&cursor, ";")) != NULL) { + if (!strstarts(entry, "CPUS=")) + continue; + if (kstrtoint(entry + 5, 10, &n) || n < 1 || n > num_online_cpus() - 1) + return -1; + } + return n; +} + +// The fair class scales its tunables by the machine's size at boot: sysctl_X = normalized_sysctl_X +// * (1 + ilog2(min(num_online_cpus(), 8))), from update_sysctl and get_update_sysctl_factor in +// kernel/sched/fair.c (SCHED_TUNABLESCALING_LOG, the default, unchanged since 2.6.32). A layout +// resumed from a larger boot would otherwise run a longer slice than the same layout booted cold, +// so the values are redone for the test CPUs plus the controller. After the domain rebuild, which +// attaches the runqueues to their root domain and reruns the kernel's own scaling (rq_online_fair). +// EEVDF has one tunable and CFS three; a kernel has one set or the other, and each value is checked +// against the formula first, so a kernel that scales differently is refused, not given a wrong one. +static unsigned int slice_factor(unsigned int cpus) { return 1 + ilog2(min_t(unsigned int, cpus, 8)); } +static void set_slice_scaling(void) { + static const char *names[] = {"sched_base_slice", "sched_min_granularity", "sched_latency", "sched_wakeup_granularity"}; + static unsigned int *sysctl[ARRAY_SIZE(names)], *normalized[ARRAY_SIZE(names)]; + static bool looked_up; + + for (int i = 0; i < ARRAY_SIZE(names); i++) { + char buf[48]; + if (!looked_up) { + snprintf(buf, sizeof(buf), "sysctl_%s", names[i]); + sysctl[i] = kstep_ksym_lookup(buf); + snprintf(buf, sizeof(buf), "normalized_sysctl_%s", names[i]); + normalized[i] = kstep_ksym_lookup(buf); + if (sysctl[i] && normalized[i] && *sysctl[i] != *normalized[i] * slice_factor(num_online_cpus())) + panic("sysctl_%s %u is not normalized %u * factor(%d): this kernel scales it differently", names[i], + *sysctl[i], *normalized[i], num_online_cpus()); + } + if (sysctl[i] && normalized[i]) + *sysctl[i] = *normalized[i] * slice_factor(kstep_test_ncpus + 1); + } + looked_up = true; +} + // NULL once applied, or what is wrong with the spec, which then changes nothing the kernel -// reads: masks are read at the rebuild and capacities are set last. "" is the machine's own -// topology with CPU 0 isolated. +// reads: masks are read at the rebuild and capacities are set last. Before the first task: a +// task's affinity and a cgroup's cpuset are drawn from CPUS, and a task left allowed on a CPU +// that a later spec made spare would run there unticked and unwatched. "" is the machine's own +// topology with CPU 0 isolated and every CPU a test CPU. const char *kstep_topo_set(const char *spec) { char buf[CPU_SPEC_LEN]; char *cursor = buf, *entry; int caps[KSTEP_NR_CPUS] = {[0 ... KSTEP_NR_CPUS - 1] = SCHED_CAPACITY_SCALE}, cpu; + int n = topo_cpus(spec); + if (n < 0) + return "CPUS is not in 1..N-1"; + kstep_test_ncpus = n; strscpy(buf, spec, sizeof(buf)); topo_init(); while ((entry = strsep(&cursor, ";")) != NULL) { @@ -266,8 +345,8 @@ const char *kstep_topo_set(const char *spec) { if (err) return err; } - for (cpu = 1; cpu < num_online_cpus(); cpu++) - set_cap(cpu, caps[cpu]); + for_each_test_cpu(c) + set_cap(c, caps[c]); // The arch's own topology state, then the rebuild. The sibling masks feed cpu_smt_mask() / // is_core_idle() directly, bypassing the topology table, so they must say the same as SMT. @@ -292,6 +371,7 @@ const char *kstep_topo_set(const char *spec) { KSYM_IMPORT(rebuild_sched_domains); KSYM_rebuild_sched_domains(); + set_slice_scaling(); return NULL; } @@ -305,7 +385,7 @@ unsigned long kstep_freq_get(int cpu) { return *per_cpu_ptr(KSYM_arch_freq_scale static void print_capacities(void) { pr_info("CPU capacities:\n"); - for (int cpu = 1; cpu < num_online_cpus(); cpu++) + for_each_test_cpu(cpu) pr_info("- CPU %d: %lu\n", cpu, arch_scale_cpu_capacity(cpu)); } diff --git a/kmod/internal.h b/kmod/internal.h index 38f60f8..fdfbcb7 100644 --- a/kmod/internal.h +++ b/kmod/internal.h @@ -18,7 +18,13 @@ #define KSTEP_NR_CPUS (32) // The test CPUs: every online CPU but 0, which is kstep's controller and runs no session task. -#define for_each_test_cpu(cpu) for (int cpu = 1, _ncpus = num_online_cpus(); cpu < _ncpus; cpu++) +// The test CPUs, 1..kstep_test_ncpus: where tasks run and what the scheduler's domains, the ticks +// and the statistics span. All of 1..N-1 from boot, so everything set up before the driver runs +// covers the whole machine; a cpu-topo spec's CPUS=K then narrows it, and the CPUs left over sit +// idle, alone at every topology level like CPU 0, with nothing sent their way. +extern int kstep_test_ncpus; +void kstep_cpu_init(void); +#define for_each_test_cpu(cpu) for (int cpu = 1, _ncpus = kstep_test_ncpus; cpu <= _ncpus; cpu++) // cpu.c bool kstep_parse_cpus(const char *list, struct cpumask *mask); // a nonempty cpulist of test CPUs diff --git a/kmod/kernel.c b/kmod/kernel.c index e704379..02a1201 100644 --- a/kmod/kernel.c +++ b/kmod/kernel.c @@ -311,7 +311,7 @@ int kstep_cgroup_create(const char *name) { char path[MAX_PATH_LENGTH]; int err; - if (scnprintf(cpuset, sizeof(cpuset), "%d-%d", 1, num_online_cpus() - 1) >= sizeof(cpuset) || + if (scnprintf(cpuset, sizeof(cpuset), "%d-%d", 1, kstep_test_ncpus) >= sizeof(cpuset) || scnprintf(path, sizeof(path), CGROUP_ROOT "%s", name) >= sizeof(path)) panic("failed to form cpuset for %s", name); diff --git a/kmod/main.c b/kmod/main.c index 04d3161..d1b4664 100644 --- a/kmod/main.c +++ b/kmod/main.c @@ -13,12 +13,9 @@ module_param_string(driver, driver_name, DRIVER_NAME_LEN, 0644); static int __init kstep_main(void) { kstep_io_init(); - if (num_online_cpus() > KSTEP_NR_CPUS) - panic("Number of online CPUs (%d) exceeds KSTEP_NR_CPUS (%d)", num_online_cpus(), - KSTEP_NR_CPUS); - TRACE_INFO("Starting %s on Linux %s", driver_name, UTS_RELEASE); kstep_driver = kstep_sym_init(driver_name); + kstep_cpu_init(); // Isolate the CPUs to avoid interference kstep_prealloc_kworkers(); @@ -29,7 +26,6 @@ static int __init kstep_main(void) { kstep_task_init(); kstep_cgroup_init(); kstep_trace_init(); // the group-alloc hook also sets min_vruntime - kstep_topo_set(""); // CPU 0 isolated from the test CPUs' sched domains (cpu.c) kstep_driver->setup(); kstep_topo_print(); diff --git a/kmod/reset.c b/kmod/reset.c index 73dfa6a..fd4f280 100644 --- a/kmod/reset.c +++ b/kmod/reset.c @@ -27,6 +27,10 @@ void kstep_reset_task(struct task_struct *p) { // https://github.com/torvalds/linux/commit/86bfbb7ce4f67a88df2639198169b685668e7349 #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0) p->se.vlag = 0; + // A fork inherits the parent's slice and the kernel rereads the sysctl only when that first slice + // ends; the parent here is a kworker with the boot's value, which cpu-topo CPUS may have changed. + KSYM_IMPORT(sysctl_sched_base_slice); + p->se.slice = *KSYM_sysctl_sched_base_slice; #endif // reset sched avg stats diff --git a/kmod/shm.c b/kmod/shm.c index b838eb1..2d61819 100644 --- a/kmod/shm.c +++ b/kmod/shm.c @@ -17,7 +17,20 @@ phys_addr_t kstep_shm_init(void) { shm = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO, get_order(sizeof(*shm))); if (!shm) panic("Failed to allocate the shared region"); - shm->hdr = (struct kstep_shm_hdr){.magic = KSTEP_SHM_MAGIC, .layout = KSTEP_SHM_LAYOUT}; + shm->hdr = (struct kstep_shm_hdr){.layout = KSTEP_SHM_LAYOUT}; +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0) + shm->hdr.features |= KSTEP_SHM_EEVDF; +#endif + // the sched-domain flag names in this kernel's bit order, so a domain's flags word decodes here + { + int bit = 0; +#define SD_FLAG(name, meta_flag) \ + if (bit < KSTEP_SHM_SD_FLAGS) \ + strscpy(shm->sd_flag[bit], &#name[3], KSTEP_SHM_SD_FLAG_LEN); \ + bit++; +#include +#undef SD_FLAG + } return virt_to_phys(shm); } @@ -151,8 +164,8 @@ static struct kstep_shm_rt_entity shm_rt_entity(struct task_struct *p, u32 task, // happens at, which nothing else reports: a task's own counters are kept in its group's queue, so // four equal tasks split two cgroups' halves without anything in the task table saying why. The // cpu controller's css is taken from the cgroup rather than the walk's, which is cgroup->self. -// Called under RCU, from a walk that runs before the seqlock is taken; the entities are stable -// because the isolated CPUs are held for the whole command. +// Called under RCU; the entities are stable because the isolated CPUs are held for the whole +// command. static void shm_group_entities(u32 cgroup, struct cgroup *cgrp, struct kstep_shm_entity *ents, u32 *nents) { #ifdef CONFIG_FAIR_GROUP_SCHED struct cgroup_subsys_state *css = rcu_dereference(cgrp->subsys[cpu_cgrp_id]); @@ -233,13 +246,14 @@ static u32 shm_collect_cgroups(struct kstep_shm_cgroup *out, struct kstep_shm_en return n; } -// The sched domains the kernel actually built, per CPU and innermost first. Read under RCU, like -// any other walk of rq->sd. A group's span comes from sched_group_span (the CPUs it covers), not -// its balance mask; sd->groups starts at the CPU's own group, so the order is the order that CPU's -// balancer sees. -static u32 shm_collect_domains(struct kstep_shm_domain *out, u32 ncpus) { +// The sched domains the kernel actually built, per CPU and innermost first, and their groups into +// the flat group table. Read under RCU, like any other walk of rq->sd. A group's span comes from +// sched_group_span (the CPUs it covers), not its balance mask; sd->groups starts at the CPU's own +// group, so the order is the order that CPU's balancer sees. +static u32 shm_collect_domains(struct kstep_shm_domain *out, struct kstep_shm_group *groups, u32 *ngroups, u32 ncpus) { u32 n = 0; + *ngroups = 0; rcu_read_lock(); for (int cpu = 1; cpu <= ncpus && n < KSTEP_SHM_DOMAINS; cpu++) { struct sched_domain *sd; @@ -253,8 +267,8 @@ static u32 shm_collect_domains(struct kstep_shm_domain *out, u32 ncpus) { memset(d, 0, sizeof(*d)); d->cpu = cpu; d->span = cpumask_bits(sched_domain_span(sd))[0]; + d->flags = sd->flags; strscpy(d->name, sd->name, sizeof(d->name)); - kstep_sd_flags_str(sd->flags, d->flags, sizeof(d->flags)); d->imbalance_pct = sd->imbalance_pct; d->balance_interval = sd->balance_interval; d->nr_balance_failed = sd->nr_balance_failed; @@ -262,18 +276,23 @@ static u32 shm_collect_domains(struct kstep_shm_domain *out, u32 ncpus) { d->cache_nice_tries = sd->cache_nice_tries; // jiffies is the mocked clock plus a fixed offset (tick_jiffies.c), so this is logical ticks d->last_balance_ago = jiffies - sd->last_balance; + d->group = *ngroups; first = sd->groups; sg = first; do { - struct kstep_shm_group *g = &d->group[d->ngroups]; + struct kstep_shm_group *g = &groups[*ngroups]; + if (*ngroups == KSTEP_SHM_GROUPS) + break; g->span = cpumask_bits(sched_group_span(sg))[0]; g->capacity = sg->sgc->capacity; g->min_capacity = sg->sgc->min_capacity; g->max_capacity = sg->sgc->max_capacity; g->weight = sg->group_weight; + (*ngroups)++; + d->ngroups++; sg = sg->next; - } while (++d->ngroups < KSTEP_SHM_GROUPS && sg != first); + } while (sg != first); n++; } } @@ -284,15 +303,13 @@ static u32 shm_collect_domains(struct kstep_shm_domain *out, u32 ncpus) { // The state after a command. The isolated CPUs are held while a command runs, so their queues // can be read directly. tasks[n - 1] is the cli's task number n; exited tasks are skipped. void kstep_shm_update(struct task_struct **tasks, int ntasks) { - u32 ncpus = min_t(u32, num_online_cpus() - 1, KSTEP_SHM_CPUS), nt = 0, nrt = 0; // table entries written + u32 ncpus = kstep_test_ncpus, nt = 0, nrt = 0; // table entries written struct kstep_shm_cgroup cgroups[KSTEP_SHM_CGROUPS]; static struct kstep_shm_entity entities[KSTEP_SHM_ENTITIES]; // 40 KB: not for the stack struct task_struct *rt_pick[KSTEP_SHM_CPUS]; u32 nentities; - u32 ncgroups = shm_collect_cgroups(cgroups, entities, &nentities); // sleeps: outside the seqlock, where a reader spins + u32 ncgroups = shm_collect_cgroups(cgroups, entities, &nentities); - WRITE_ONCE(shm->hdr.gen, shm->hdr.gen + 1); // odd: updating - smp_wmb(); for (int cpu = 1; cpu <= ncpus; cpu++) { struct rq *rq = cpu_rq(cpu); int curr = 0; @@ -392,9 +409,7 @@ void kstep_shm_update(struct task_struct **tasks, int ntasks) { shm->hdr.ncpus = ncpus; shm->hdr.ntasks = nt; shm->hdr.ncgroups = ncgroups; - shm->hdr.ndomains = shm_collect_domains(shm->domain, ncpus); + shm->hdr.ndomains = shm_collect_domains(shm->domain, shm->group, &shm->hdr.ngroups, ncpus); shm->hdr.nentities = nentities; shm->hdr.nrt_entities = nrt; - smp_wmb(); - WRITE_ONCE(shm->hdr.gen, shm->hdr.gen + 1); // even: consistent } diff --git a/kmod/shm.h b/kmod/shm.h index 8efed4d..79420bc 100644 --- a/kmod/shm.h +++ b/kmod/shm.h @@ -1,41 +1,47 @@ -// The region of guest memory the host reads directly (shm.c writes it; crates/core/src/shm.rs -// decodes it, with these structs generated from this header): the machine's state, rewritten -// after every cli command. gen is a seqlock over the state: odd while an update is in progress, -// even and unchanged around a consistent read. The coverage map (cov.c) is a region of its own, -// so adding a field here never moves it; both addresses are reported on the cli's ready line. Trace events are not here: scheduler hooks write -// them as JSON records on the driver's channel (io.c). +// The region of guest memory the host reads directly: the machine's state, rewritten by shm.c +// after every cli command and decoded by crates/core/src/shm.rs with structs generated from this +// header. There is one writer, the controller, and every reader waits for the command's reply +// before copying the region, so no lock or generation count guards it. The coverage map (cov.c) is +// a region of its own, so adding a field here never moves it; both addresses are reported on the +// cli's ready line. Trace events are not here: scheduler hooks write them as JSON records on the +// driver's channel (io.c). #pragma once #include -// As many as the module itself allows (KSTEP_NR_CPUS): the cap is the kmod's, not this region's. -// KSTEP_SHM_GROUPS must track it -- a domain can have one group per CPU -- and KSTEP_SHM_DOMAINS -// is one record per (CPU, level) over the five topology levels. -#define KSTEP_SHM_CPUS 32 // isolated CPUs 1..32 +// Bump whenever a struct below changes, in size or in meaning: a decoder built from another +// revision of this header refuses the region instead of misreading it. +#define KSTEP_SHM_LAYOUT 10 + +// hdr.features: what this kernel's scheduler has, so a reader shows what exists rather than +// knowing kernel versions. EEVDF (6.6+): the fair entities' lag, deadline, eligibility and pick. +#define KSTEP_SHM_EEVDF (1u << 0) + +// Table capacities. CPUs: as many as the module allows (KSTEP_NR_CPUS). Domains: one record per +// (test CPU, topology level) over the five levels; their balancing groups are one flat table, +// each domain naming its run of it -- a real hierarchy has a few hundred at most, and a domain +// whose groups do not fit reports the ones that did. Entities: every task's plus one group entity +// per (cgroup, CPU). +#define KSTEP_SHM_CPUS 32 #define KSTEP_SHM_TASKS 64 #define KSTEP_SHM_CGROUPS 16 -#define KSTEP_SHM_ENTITIES (KSTEP_SHM_TASKS + KSTEP_SHM_CGROUPS * KSTEP_SHM_CPUS) // every task's, plus one group entity per (cgroup, CPU) -#define KSTEP_SHM_DOMAINS (KSTEP_SHM_CPUS * 5) // one per (CPU, level) -#define KSTEP_SHM_GROUPS KSTEP_SHM_CPUS // a domain's balancing groups, at most one per CPU +#define KSTEP_SHM_ENTITIES (KSTEP_SHM_TASKS + KSTEP_SHM_CGROUPS * KSTEP_SHM_CPUS) +#define KSTEP_SHM_DOMAINS (KSTEP_SHM_CPUS * 5) +#define KSTEP_SHM_GROUPS 1024 +#define KSTEP_SHM_SD_FLAGS 32 // sched-domain flag bits this kernel can have (sd_flags.h has ~14) +#define KSTEP_SHM_SD_FLAG_LEN 24 #define KSTEP_COV_SIZE (1 << 16) // cov.c's edge map, saturating byte counts; crates/core/src/shm.rs COV_SIZE -// The host decodes the region with structs generated from this header (crates/core/src/shm.rs), -// so the header only says how many records each table holds. magic and layout come first, and a -// host checks both before trusting anything else: bump KSTEP_SHM_LAYOUT whenever a struct below -// changes, in size or in meaning. gen is the seqlock. -#define KSTEP_SHM_MAGIC 0x5054536b // "kSTP", little endian -#define KSTEP_SHM_LAYOUT 8 - // The machine and the tasks first, then one pair of tables per scheduling class: the class's // queue on each CPU (ncpus records, like the CPU table), and what is on those queues. A task's // record says which class it is under; what that class makes of it is in the class's own member // table, joined by task number. Nothing about one class sits in the CPU or task record, so adding -// a class is adding a pair here. +// a class is adding a pair here. Struct padding is the compiler's, mirrored by the generated +// structs, so no field is spent on it. struct kstep_shm_hdr { - u32 magic, layout; - u32 gen; + u32 layout, features; u32 timestamp; // logical ticks - u32 ncpus, ntasks, ncgroups, ndomains; + u32 ncpus, ntasks, ncgroups, ndomains, ngroups; u32 nentities; // the fair class: entities queued under the root cfs_rq of any CPU u32 nrt_entities; // the real-time class: tasks on any rt_rq }; @@ -43,11 +49,11 @@ struct kstep_shm_hdr { // The runqueue itself, and nothing any one class owns: what the CPU is doing, and when its // balancer next runs. What each class queued there looks like is in that class's table. struct kstep_shm_cpu { + u64 nr_running, nr_switches; u32 cpu, curr; // curr: kSTEP task number running there, 0 for none or another process u32 idle, capacity; u32 freq; // arch_freq_scale: the current frequency, which cpufreq moves under a running system u32 next_balance_in; // ticks until rq->next_balance comes due, 0 when it already has - u64 nr_running, nr_switches; }; enum kstep_shm_task_state { KSTEP_TASK_RUNNING, KSTEP_TASK_RUNNABLE, KSTEP_TASK_SLEEPING, KSTEP_TASK_BLOCKED }; @@ -64,8 +70,6 @@ enum kstep_shm_task_state { KSTEP_TASK_RUNNING, KSTEP_TASK_RUNNABLE, KSTEP_TASK_ // CPU: the queue picks between entities without caring which, so both records carry this block // and the page decodes it once. struct kstep_shm_se { - u32 flags; - u32 share; // of the CPU, in 1/1024: this weight over its queue's, times the parent entity's share, up to the root -- the walk update_cfs_rq_h_load makes over load averages, made over weights u64 weight; // scale_load_down: the units the nice table is written in, nice 0 being 1024 u64 sum_exec_runtime; // Signed: the kernel keeps vruntimes unsigned and compares them as (s64)(a - b), so a woken task @@ -77,6 +81,8 @@ struct kstep_shm_se { // EEVDF's eligibility is lag >= 0. Live from avg_vruntime while queued; the kernel's saved // se->vlag, which place_entity will restore, while not. s64 lag; + u32 flags; + u32 share; // of the CPU, in 1/1024: this weight over its queue's, times the parent entity's share, up to the root -- the walk update_cfs_rq_h_load makes over load averages, made over weights }; // A task's identity and its scheduling attributes -- the sched_attr fields every class reads or @@ -84,33 +90,31 @@ struct kstep_shm_se { // holds it is `policy`; that class's view of it is the record with this task number in the // class's member table, and a task is in exactly one of those at a time. struct kstep_shm_task { + u64 cpus; // allowed CPUs as a bitmask + u64 sum_exec_runtime; // CPU time given so far, under whichever class held it at the time u32 task, state, cpu, policy; // policy: the kernel's SCHED_* number s32 nice; // kept, though only the fair classes read it u32 rt_priority; // 1..99 under fifo/rr, 0 otherwise: the kernel zeroes it on the way back u32 cgroup; // index into the cgroup table, as an entity's - u32 reserved; - u64 cpus; // allowed CPUs as a bitmask - u64 sum_exec_runtime; // CPU time given so far, under whichever class held it at the time }; // One entry per live cgroup, the root ("/") first: what the kernel holds, not what a driver // last wrote, so a weight or cpuset changed behind a program's back still shows up. This is the // cgroup's configuration; what the scheduler does with it is in the entity table below. struct kstep_shm_cgroup { - char path[40]; // as cgroup_path() reports it, e.g. "/a/b" u64 cpus; // cpuset.cpus.effective as a bitmask u32 weight; // cpu.weight, 0 where the cpu controller is not on - u32 reserved; + char path[40]; // as cgroup_path() reports it, e.g. "/a/b" }; // The fair class's root queue on one CPU: rq->cfs. The balancer reads this, not the runqueue: // nr_running counts everything queued, h_nr_runnable only what the fair class will run, and a // real-time task, or one left behind by delayed dequeue, is in one and not the other. struct kstep_shm_cfs { - u32 cpu, reserved; s64 min_vruntime; // signed, as an entity's vruntime u64 util_avg, load_avg, runnable_avg; u64 h_nr_runnable; // cfs_rq->h_nr_runnable: runnable tasks as the balancer counts them + u32 cpu; }; // One sched_entity as the fair class sees it: a task's, or a cgroup's group entity on one CPU. @@ -122,10 +126,10 @@ struct kstep_shm_cfs { // The root task group owns no entity -- its cfs_rq is the runqueue's own -- so it has no rows. // A task's record is here only while a fair policy holds it; its cgroup is the queue it is on. struct kstep_shm_entity { + struct kstep_shm_se se; u32 task; // kSTEP task number, 0 for a cgroup's entity u32 cgroup; // index into the cgroup table - u32 cpu, reserved; - struct kstep_shm_se se; + u32 cpu; }; // The real-time class's queue on one CPU: rq->rt. It picks the head of the highest non-empty @@ -134,10 +138,10 @@ struct kstep_shm_entity { // (sched_rt_period_us, 1 s), and throttled says the period's share is spent. Under // CONFIG_RT_GROUP_SCHED these are the root task group's; the fields are zero without it. struct kstep_shm_rt { + u64 rt_time, rt_runtime; // ns u32 cpu, nr_running; u32 highest_prio; // rt_rq->highest_prio.curr: the kernel's prio, 0 (highest) .. 98; 100 when empty u32 throttled; - u64 rt_time, rt_runtime; // ns }; #define KSTEP_RT_ON_RQ 1 // rt.on_rq: enqueued @@ -151,13 +155,29 @@ struct kstep_shm_rt { struct kstep_shm_rt_entity { u32 task, cpu; u32 flags, position; - u32 time_slice, reserved; + u32 time_slice; }; // One sched domain as the kernel built it, per CPU, innermost first -- not the topology the cli // was asked for: Linux drops a level whose span adds nothing (sd_degenerate), so a level named in // `cpu-topo` may simply not be here, which is the whole reason this is worth reporting. The -// balancing knobs are the ones that decide whether a given tick balances at all. +// balancing knobs are the ones that decide whether a given tick balances at all. flags is sd->flags +// as the kernel numbers its bits; the names, in that bit order, are in kstep_shm.sd_flag, written +// at init from the kernel's own sd_flags.h, so a reader never carries a table that can drift from +// the kernel in front of it (SD_SHARE_PKG_RESOURCES became SD_SHARE_LLC, and such renames are +// routine). Not here on purpose: sd->shared's nr_busy_cpus and has_idle_cores, which +// select_idle_sibling reads on a wakeup. Only the NOHZ path maintains nr_busy_cpus and this kernel +// is built without CONFIG_NO_HZ, so it would read "nothing is idle" on an empty machine. +struct kstep_shm_domain { + u64 span, flags; + u32 cpu; + u32 group, ngroups; // this domain's balancing groups: the group table from `group`, `ngroups` of them, the CPU's own group first as sd->groups has it + u32 imbalance_pct, balance_interval, busy_factor, cache_nice_tries; + u32 nr_balance_failed; // per CPU: consecutive failures here, which escalate to active balancing + u32 last_balance_ago; // ticks since this CPU last balanced at this level + char name[8]; // the topology level: SMT, CLS, MC, PKG, NODE +}; + struct kstep_shm_group { u64 span; u32 capacity; // sgc->capacity: the group's total, after RT/DL pressure @@ -165,27 +185,14 @@ struct kstep_shm_group { u32 weight; // CPUs in the group }; -struct kstep_shm_domain { - u32 cpu, ngroups; - u64 span; - char name[8]; // the topology level: SMT, CLS, MC, PKG, NODE - char flags[160]; // SD_* names as the kernel has them, "SD_" stripped, e.g. "SHARE_LLC, PREFER_SIBLING" - u32 imbalance_pct, balance_interval, busy_factor, cache_nice_tries; - u32 nr_balance_failed; // per CPU: consecutive failures here, which escalate to active balancing - u32 last_balance_ago; // ticks since this CPU last balanced at this level - // Not here on purpose: sd->shared's nr_busy_cpus and has_idle_cores, which select_idle_sibling - // reads on a wakeup. Only the NOHZ path maintains nr_busy_cpus (set_cpu_sd_state_busy/idle) and - // this kernel is built without CONFIG_NO_HZ, so it sits at the domain's weight forever; measured - // has_idle_cores likewise never leaves 0. Both would read "nothing is idle" on an empty machine. - struct kstep_shm_group group[KSTEP_SHM_GROUPS]; -}; - struct kstep_shm { struct kstep_shm_hdr hdr; + char sd_flag[KSTEP_SHM_SD_FLAGS][KSTEP_SHM_SD_FLAG_LEN]; // SD_* names by bit, "SD_" stripped; "" past the last struct kstep_shm_cpu cpu[KSTEP_SHM_CPUS]; struct kstep_shm_task task[KSTEP_SHM_TASKS]; struct kstep_shm_cgroup cgroup[KSTEP_SHM_CGROUPS]; struct kstep_shm_domain domain[KSTEP_SHM_DOMAINS]; + struct kstep_shm_group group[KSTEP_SHM_GROUPS]; struct kstep_shm_cfs cfs[KSTEP_SHM_CPUS]; struct kstep_shm_entity entity[KSTEP_SHM_ENTITIES]; struct kstep_shm_rt rt[KSTEP_SHM_CPUS]; diff --git a/kmod/task.c b/kmod/task.c index 7ee0a96..f9830c6 100644 --- a/kmod/task.c +++ b/kmod/task.c @@ -182,7 +182,7 @@ struct task_struct *kstep_task_create(void) { wait_for_completion(&kstep_task_parked); TRACE_INFO("Task %d is ready", p->pid); char cpus[16]; - snprintf(cpus, sizeof(cpus), "1-%d", num_online_cpus() - 1); + snprintf(cpus, sizeof(cpus), "1-%d", kstep_test_ncpus); if (kstep_task_set_affinity(p, cpus)) panic("Failed to set CPU affinity for task %d to CPUs %s", p->pid, cpus); kstep_reset_task(p); diff --git a/website b/website index 37ce27c..aa6966e 160000 --- a/website +++ b/website @@ -1 +1 @@ -Subproject commit 37ce27c11ef2bca6f1f070e02702046e4bd44e64 +Subproject commit aa6966e7898b6f7669e33b88b287a0d7f140b2be