diff --git a/issues/every-interrupt-lands-on-the-boot-cpu.md b/issues/every-interrupt-lands-on-the-boot-cpu.md index ec515548c1b..37d7ed41f06 100644 --- a/issues/every-interrupt-lands-on-the-boot-cpu.md +++ b/issues/every-interrupt-lands-on-the-boot-cpu.md @@ -46,12 +46,12 @@ the machine's, and every device shares it. ## Instrument, and the baseline (2026-08-22) `kernel/src/irq_census.rs` counts every delivery per CPU per source in -`PerCpu`, one `add qword ptr gs:[], 1` for the machine's total and one for -the source. `irq: cpuN total=… timer=… …` is printed per CPU beside the +`PerCpu`, one `add qword ptr gs:[], 1` for the source; a CPU's total is +their sum. `irq: cpuN timer=… kick=… …` is printed per CPU beside the process-exit census, on `SYS_SHUTDOWN` and on the blocked-task dump; `common::irqcensus` aggregates every guest's newest line into the suite's own summary, so a CI shard's log carries the number without `--nocapture`. -`irq_census_conservation` gates both the arithmetic and the present-state fact. +`irq_census_conservation` gates the present-state fact. A guest that boots and runs no program reaches no process exit and prints no census, which is why the reporting counts are short of the boots. Both columns diff --git a/issues/the-irq-census-judge-reds-on-two-exits-stamped-in-the-other-order.md b/issues/the-irq-census-judge-reds-on-two-exits-stamped-in-the-other-order.md new file mode 100644 index 00000000000..7f654801a41 --- /dev/null +++ b/issues/the-irq-census-judge-reds-on-two-exits-stamped-in-the-other-order.md @@ -0,0 +1,37 @@ +--- +status: open +kind: defect +opened: 2026-10-04 +--- + +# The `irq_census` judge reds on two exits' lines stamped in the other order from their reads + +The judge `irq_census` (`tests/toyos.rs`) compares lines from different process +exits as if the capture listed them in the order their counters were read. It +does not. Each exit prints `irq_census::log_census` and then +`tlb::log_census` (`kernel/src/process.rs`), and nothing serialises two exits on +two CPUs. `log::emit` reads its arguments and formats them before it stamps the +record (`kernel/src/log/mod.rs`), each CPU writes its own shard, and +`log::read::drain_ordered` merges the shards by stamp. So exit X can read a +counter before exit Y and still be stamped after Y. + +Two of the judge's checks then red with no kernel defect: + +- **The per-source monotonic check.** X reads cpu1's census, then Y reads it + with one more `kick`. Y is stamped first. The capture shows cpu1's `kick` + going backwards. +- **The issuer checks on `tlb: shootdowns=`.** Y loads `ISSUED` and gets T1. + X then loads T2 > T1, swaps `REPORTED` to T2 and logs. Y's swap returns T2, + which is not T1, so Y logs `shootdowns=T1` after X's T2. The capture shows + `T2` then `T1`, which reds "the issuer census went backwards". The last + `tlb:` line is then T1, and a newer `irq:` line's `tlb` count can exceed it, + which reds "some path shoots down without being counted". + +Neither has been seen red. The recorded red the same window produced was the +deleted total/sources check (#734), where the reads were within one line. + +Owner: the harness, `irq_census` in `tests/toyos.rs`. + +**Exit:** a host test feeds the judge two exits' `irq:` and `tlb:` lines in +stamp order, with the later stamp carrying the earlier read as above, and the +judge stays green. diff --git a/issues/the-spurious-and-unclaimed-selftests-took-interrupts-after-check-cannot-fail.md b/issues/the-spurious-and-unclaimed-selftests-took-interrupts-after-check-cannot-fail.md new file mode 100644 index 00000000000..31b9a5899c0 --- /dev/null +++ b/issues/the-spurious-and-unclaimed-selftests-took-interrupts-after-check-cannot-fail.md @@ -0,0 +1,23 @@ +--- +status: open +kind: defect +opened: 2026-10-04 +--- + +# The spurious and unclaimed selftests' "took interrupts after it" check cannot fail + +`spurious::selftest` (`kernel/src/arch/x86_64/idt/spurious.rs`) and +`unclaimed::selftest` (`kernel/src/arch/x86_64/idt/unclaimed.rs`) each claim to +confirm the CPU still takes interrupts after the probe vector. They read +`taken_before = deliveries_total(cpu)` before `apic::send_self`. The probe's +own delivery is counted in that total. Once `delivered` has held, the probe's +source count is past `before`, so `deliveries_total(cpu) > taken_before` is +already true at the first poll. A CPU left deaf by the probe's handler passes +the check, and the `3/3` line says "the CPU took interrupts after it" either +way. + +Owner: the LAPIC selftests, `kernel/src/arch/x86_64/idt/`. + +**Exit:** a mutation that leaves the CPU taking no interrupt once the probe's +handler returns makes each selftest print `FAILED`, and the `selftests` row in +`tests/toyos.rs` that boots both actuators reds on it. diff --git a/kernel/src/arch/aarch64/percpu.rs b/kernel/src/arch/aarch64/percpu.rs index 911c7092758..3a97e5930f3 100644 --- a/kernel/src/arch/aarch64/percpu.rs +++ b/kernel/src/arch/aarch64/percpu.rs @@ -56,8 +56,8 @@ pub struct PerCpu { syscall_task: AtomicU64, /// This CPU's [`log::Shard`]: the boot shard on CPU 0. log_shard: &'static log::Shard, - /// One counter per `irq_census::Source`, and the total. - irq_counts: [AtomicU64; crate::irq_census::SLOTS], + /// One counter per `irq_census::Source`. + irq_counts: [AtomicU64; crate::irq_census::Source::COUNT], } /// This CPU's block. @@ -94,7 +94,7 @@ pub fn alloc(cpu_id: u32) -> &'static PerCpu { syscall_sp: AtomicU64::new(0), syscall_task: AtomicU64::new(NO_SYSCALL), log_shard: log::shard_for(cpu_id), - irq_counts: [const { AtomicU64::new(0) }; crate::irq_census::SLOTS], + irq_counts: [const { AtomicU64::new(0) }; crate::irq_census::Source::COUNT], })); crate::irq_census::publish(cpu_id, block.irq_counts.as_ptr()); block @@ -249,14 +249,13 @@ pub fn reserve_log_slot(guard: &crate::arch::IrqGuard) -> (*const log::Shard, u6 /// One delivery of `source`, counted in this CPU's block. pub(super) fn irq_took(source: crate::irq_census::Source) { let counts = &this().irq_counts; - counts[crate::irq_census::TOTAL].fetch_add(1, Relaxed); - counts[1 + source as usize].fetch_add(1, Relaxed); + counts[source as usize].fetch_add(1, Relaxed); } -/// Two of this CPU's interrupt counters. -pub fn irq_counts_here(first: usize, second: usize) -> (u64, u64) { +/// This CPU's interrupt counters. +pub fn irq_counts_here() -> [u64; crate::irq_census::Source::COUNT] { let counts = &this().irq_counts; - (counts[first].load(Relaxed), counts[second].load(Relaxed)) + core::array::from_fn(|index| counts[index].load(Relaxed)) } #[inline] diff --git a/kernel/src/arch/x86_64/idt/timer.rs b/kernel/src/arch/x86_64/idt/timer.rs index d26595a3877..312af4ed4ad 100644 --- a/kernel/src/arch/x86_64/idt/timer.rs +++ b/kernel/src/arch/x86_64/idt/timer.rs @@ -41,8 +41,7 @@ pub(super) extern "sysv64" fn timer_entry() { "iretq", "2:", - // No Rust half on this branch, so these two `add`s inline what `timer_handler` does for Ring 3; flags are dead after the `test` above, so none are saved. - "add qword ptr gs:[{irq_total}], 1", + // No Rust half on this branch, so this `add` inlines what `timer_handler` does for Ring 3; flags are dead after the `test` above, so none are saved. "add qword ptr gs:[{irq_timer}], 1", "push rax", "push rcx", @@ -92,10 +91,7 @@ pub(super) extern "sysv64" fn timer_entry() { quantum_ticks = sym crate::arch::apic::TIMER_TICKS, need_resched = const crate::arch::percpu::OFF_NEED_RESCHED, ring0_fires = const crate::arch::percpu::OFF_RING0_TIMER_FIRES, - irq_total = const crate::arch::percpu::irq_slot_offset(crate::irq_census::TOTAL), - irq_timer = const crate::arch::percpu::irq_slot_offset( - 1 + crate::irq_census::Source::Timer as usize - ), + irq_timer = const crate::arch::percpu::irq_slot_offset(crate::irq_census::Source::Timer as usize), ); } diff --git a/kernel/src/arch/x86_64/percpu.rs b/kernel/src/arch/x86_64/percpu.rs index ddc510ba10b..747e719d3b9 100644 --- a/kernel/src/arch/x86_64/percpu.rs +++ b/kernel/src/arch/x86_64/percpu.rs @@ -114,8 +114,8 @@ pub struct PerCpu { /// Non-zero inside this CPU's NMI handler, written only by `arch::idt::nmi`'s entry; IST2 isn't re-entrant, so this proves no second NMI lands on it. nmi_active: u32, ap_token: u32, - /// Interrupt deliveries, one counter per `irq_census::Source`; written only by `irq_census::irq_took!`, kept last so growing `SLOTS` moves nothing else. - pub irq_counts: [AtomicU64; crate::irq_census::SLOTS], + /// Interrupt deliveries, one counter per `irq_census::Source`; written only by `irq_census::irq_took!`, kept last so a new source moves nothing else. + pub irq_counts: [AtomicU64; crate::irq_census::Source::COUNT], } const GDT_ENTRIES: [u64; 7] = [ @@ -374,7 +374,7 @@ fn alloc_percpu(cpu_id: u32) -> *mut PerCpu { log_shard: log::shard_for(cpu_id) as *const log::Shard as u64, nmi_active: 0, ap_token: 0, - irq_counts: [const { AtomicU64::new(0) }; crate::irq_census::SLOTS], + irq_counts: [const { AtomicU64::new(0) }; crate::irq_census::Source::COUNT], }, ); } @@ -725,20 +725,18 @@ pub const fn irq_slot_offset(index: usize) -> u32 { OFF_IRQ_COUNTS + (index as u32) * 8 } -/// Records one delivery of `$source` as two lock-free `add`s to this CPU's own gs: slots. -/// A macro, not a function: the two offsets must be asm immediates, not const-generic values an optimiser could relax. +/// Records one delivery of `$source` as one lock-free `add` to this CPU's own gs: slot. +/// A macro, not a function: the offset must be an asm immediate, not a const-generic value an optimiser could relax. macro_rules! irq_took { ($source:ident) => {{ - // SAFETY: both slots are this CPU's own counter block per `arch::percpu`, and the caller is an interrupt handler, so `GS_BASE` already points at this CPU's `PerCpu`. + // SAFETY: the slot is this CPU's own counter block per `arch::percpu`, and the caller is an interrupt handler, so `GS_BASE` already points at this CPU's `PerCpu`. unsafe { ::core::arch::asm!( - "add qword ptr gs:[{total}], 1", "add qword ptr gs:[{source}], 1", - total = const $crate::arch::percpu::irq_slot_offset($crate::irq_census::TOTAL), source = const $crate::arch::percpu::irq_slot_offset( - 1 + $crate::irq_census::Source::$source as usize + $crate::irq_census::Source::$source as usize ), - // no `nomem` because both instructions write; no `preserves_flags` because `add` clobbers flags. + // no `nomem` because the instruction writes; no `preserves_flags` because `add` clobbers flags. options(nostack), ); } @@ -747,26 +745,24 @@ macro_rules! irq_took { pub(crate) use irq_took; -/// Two of this CPU's interrupt counters, read straight off `gs:` with one load +/// This CPU's interrupt counters, read straight off `gs:` with one load /// each — the form a CPU inside an NMI may use. -pub fn irq_counts_here(first: usize, second: usize) -> (u64, u64) { - let a: u64; - let b: u64; - // SAFETY: both slots are this CPU's own counter block, and `GS_BASE` points - // at the running CPU's `PerCpu` in every context this is read from; both - // indices are below `irq_census::SLOTS`, asserted by the callers' constants. - unsafe { - core::arch::asm!( - "mov {a}, qword ptr gs:[{first}]", - "mov {b}, qword ptr gs:[{second}]", - a = out(reg) a, - b = out(reg) b, - first = in(reg) u64::from(irq_slot_offset(first)), - second = in(reg) u64::from(irq_slot_offset(second)), - options(nostack, readonly, preserves_flags), - ); - } - (a, b) +pub fn irq_counts_here() -> [u64; crate::irq_census::Source::COUNT] { + core::array::from_fn(|index| { + let count: u64; + // SAFETY: the slot is this CPU's own counter block, and `GS_BASE` points + // at the running CPU's `PerCpu` in every context this is read from; + // `from_fn` keeps `index` below the block's length. + unsafe { + core::arch::asm!( + "mov {count}, qword ptr gs:[{at}]", + count = out(reg) count, + at = in(reg) u64::from(irq_slot_offset(index)), + options(nostack, readonly, preserves_flags), + ); + } + count + }) } /// This CPU's preempt count: the per-CPU word `crate::preempt` keeps, read and diff --git a/kernel/src/irq_census.rs b/kernel/src/irq_census.rs index 98f1093f809..196ed8ee04a 100644 --- a/kernel/src/irq_census.rs +++ b/kernel/src/irq_census.rs @@ -1,6 +1,6 @@ //! Per-CPU, per-source interrupt delivery counts, kept in `PerCpu` for a lock-free `add`. -//! `total` increments separately from the per-source counts, so a missing source -//! increment shows as `total` exceeding their sum; `irq_census_conservation` checks it. +//! One word per delivery and no total beside it: a CPU's total is the sum of its sources, +//! because a second word another CPU reads between the two `add`s is a census that does not add up. //! Device interrupts land on the boot CPU only; the timer and shootdown IPI are per-CPU. use core::fmt; @@ -56,11 +56,10 @@ impl Source { ]; } -/// One `u64` per source plus the total, in each CPU's own per-CPU block. -pub const SLOTS: usize = 1 + Source::COUNT; - -/// Index of the machine's own total inside a CPU's block. -pub const TOTAL: usize = 0; +/// Every source's count summed, bar `except`'s. +fn sum_except(counts: &[u64; Source::COUNT], except: Option) -> u64 { + counts.iter().sum::() - except.map_or(0, |source| counts[source as usize]) +} // Counted where each is taken, by the architecture's handlers // (`arch::percpu::irq_took!`), into this CPU's own block. @@ -77,12 +76,12 @@ pub(crate) fn publish(cpu_id: u32, block: *const AtomicU64) { } /// One CPU's counters, or `None` if that CPU has never been built. -fn read(cpu: u32) -> Option<[u64; SLOTS]> { +fn read(cpu: u32) -> Option<[u64; Source::COUNT]> { let base = BLOCKS.get(cpu as usize)?.load(Ordering::Acquire) as *const AtomicU64; if base.is_null() { return None; } - let mut out = [0u64; SLOTS]; + let mut out = [0u64; Source::COUNT]; for (i, slot) in out.iter_mut().enumerate() { // SAFETY: `base.add(i)` points at a live, in-bounds, single-writer counter word. *slot = unsafe { (*base.add(i)).load(Ordering::Relaxed) }; @@ -104,13 +103,13 @@ impl fmt::Display for Fields<'_> { /// One CPU's delivery count for `source`, or `None` if that CPU has never been built. pub fn deliveries(cpu: u32, source: Source) -> Option { - read(cpu).map(|counts| counts[1 + source as usize]) + read(cpu).map(|counts| counts[source as usize]) } /// Every CPU's total delivery count, or `None` if that CPU has never been built. #[cfg(feature = "boot-actuators")] pub fn deliveries_total(cpu: u32) -> Option { - read(cpu).map(|counts| counts[TOTAL]) + read(cpu).map(|counts| sum_except(&counts, None)) } /// **One CPU's progress: every interrupt it has taken except the NMI.** @@ -120,31 +119,26 @@ pub fn deliveries_total(cpu: u32) -> Option { /// *as* an NMI and has already counted itself by the time it reads this, so a /// total including it moves on every sample and no CPU is ever stuck. /// -/// One published pointer and two relaxed loads: no lock, so the CPU sealing a +/// One published pointer and relaxed loads: no lock, so the CPU sealing a /// record about a sibling can read this about it. pub fn taken_by(cpu: u32) -> Option { - read(cpu).map(|counts| counts[TOTAL].saturating_sub(counts[1 + Source::Nmi as usize])) + read(cpu).map(|counts| sum_except(&counts, Some(Source::Nmi))) } /// [`taken_by`] for the CPU asking, read straight off its own block — the one /// form a CPU inside an NMI may use, since it needs neither the published /// pointer array nor a bounds check on a `cpu_id` it is standing on. pub fn taken_here() -> u64 { - let (total, nmis) = percpu::irq_counts_here(TOTAL, 1 + Source::Nmi as usize); - total.saturating_sub(nmis) + sum_except(&percpu::irq_counts_here(), Some(Source::Nmi)) } -/// Logs one `irq: cpuN total=… =…` line per online CPU; counts are cumulative since boot. +/// Logs one `irq: cpuN =…` line per online CPU; counts are cumulative since boot. /// A `mask-windows` kernel follows each with that CPU's `windows:` line. /// Allocates nothing, takes no lock, touches no device. pub fn log_census() { for cpu in 0..crate::smp::cpu_count() { let Some(counts) = read(cpu) else { continue }; - crate::log!( - "irq: cpu{cpu} total={}{}", - counts[TOTAL], - Fields(&counts[TOTAL + 1..]) - ); + crate::log!("irq: cpu{cpu}{}", Fields(&counts)); #[cfg(feature = "mask-windows")] crate::windows::log_cpu(cpu); } diff --git a/tests/checks.rs b/tests/checks.rs index 04f50c4bd14..1713391032b 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -412,7 +412,7 @@ mod checks { use common::irqcensus::{windows_under, Measured}; let census = |cpu: u32| { format!( - "[kernel 0.1 cpu0] irq: cpu{cpu} total=0 timer=0 kick=0 xhci=0 userdev=0 sound=0 i8042=0 \ + "[kernel 0.1 cpu0] irq: cpu{cpu} timer=0 kick=0 xhci=0 userdev=0 sound=0 i8042=0 \ dmafault=0 hda=0 tlb=0 nmi=0 spurious=0 unclaimed=0\n" ) }; diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs index 656bc7d50a0..a4bfb247699 100644 --- a/tests/common/irqcensus.rs +++ b/tests/common/irqcensus.rs @@ -1,7 +1,7 @@ //! The kernel's interrupt census, and the windows a `mask-windows` kernel //! prints beside it, read back on the host. //! -//! The guest prints `irq: cpuN total=… timer=… …` per online CPU whenever a +//! The guest prints `irq: cpuN timer=… kick=… …` per online CPU whenever a //! process exits, on `SYS_SHUTDOWN` and on the blocked-task dump //! (`kernel/src/irq_census.rs`). The counters are cumulative since boot, so the //! **last** line a capture holds for a CPU is that boot's whole census. @@ -39,7 +39,6 @@ pub const DEVICE_SOURCES: [&str; 6] = ["xhci", "userdev", "sound", "i8042", "dma #[derive(Clone, Debug, PartialEq, Eq)] pub struct Census { pub cpu: u32, - pub total: u64, /// Indexed the same as [`SOURCES`]. pub by_source: [u64; SOURCES.len()], } @@ -71,19 +70,18 @@ impl Census { .map_err(|_| format!("field {field:?} has no count in {rest:?}"))?; named.push((name, value)); } - let want: Vec<&str> = std::iter::once("total").chain(SOURCES).collect(); let got: Vec<&str> = named.iter().map(|(n, _)| *n).collect(); - if got != want { + if got != SOURCES { return Err(format!( - "census fields {got:?}, want {want:?} — the kernel's `Source::NAMES` and \ + "census fields {got:?}, want {SOURCES:?} — the kernel's `Source::NAMES` and \ `common::irqcensus::SOURCES` disagree" )); } let mut by_source = [0u64; SOURCES.len()]; - for (slot, (_, value)) in by_source.iter_mut().zip(&named[1..]) { + for (slot, (_, value)) in by_source.iter_mut().zip(&named) { *slot = *value; } - Ok(Self { cpu, total: named[0].1, by_source }) + Ok(Self { cpu, by_source }) } pub fn source(&self, name: &str) -> u64 { @@ -91,10 +89,8 @@ impl Census { self.by_source[i] } - /// Every source summed. Equal to [`Self::total`] on a census that adds up — - /// which is the whole of what `irq_census_conservation` asks, because the - /// kernel counts the total apart from the sources rather than deriving it. - pub fn sum_of_sources(&self) -> u64 { + /// Every interrupt this CPU took: the kernel keeps no total apart from its sources. + pub fn total(&self) -> u64 { self.by_source.iter().sum() } } @@ -326,11 +322,11 @@ fn guests() -> Vec { let seen = SEEN.lock().expect("census map poisoned"); seen.values() .filter_map(|by_cpu| { - let total: u64 = by_cpu.values().map(|c| c.total).sum(); + let total: u64 = by_cpu.values().map(Census::total).sum(); if total == 0 { return None; } - let boot = by_cpu.get(&0).map_or(0, |c| c.total); + let boot = by_cpu.get(&0).map_or(0, Census::total); let mut per_source = [(0u64, 0u64); SOURCES.len()]; for census in by_cpu.values() { for (slot, count) in per_source.iter_mut().zip(census.by_source) { diff --git a/tests/toyos.rs b/tests/toyos.rs index bbbb6aaef34..f5b3f9e64ea 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2686,7 +2686,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { /// The interrupt census adds up, is monotonic, and every device delivery is /// still cpu0's. fn irq_census(capture: &str) -> Result<(), String> { - use common::irqcensus::{Census, DEVICE_SOURCES}; + use common::irqcensus::{Census, DEVICE_SOURCES, SOURCES}; // Every line, in order, so a later census can be compared with an // earlier one on the same CPU. let mut lines: Vec = Vec::new(); @@ -2704,44 +2704,30 @@ fn irq_census(capture: &str) -> Result<(), String> { )); } - // 1. The law. `total` is counted by its own increment beside each - // source's, never derived from them, so this is a real - // conservation statement: a source whose increment went missing - // leaves the total ahead of the sum. - for census in &lines { - if census.total != census.sum_of_sources() { - return Err(format!( - "cpu{} counted {} interrupt(s) and attributed {} to sources — a source \ - is not being counted: {census:?}", - census.cpu, - census.total, - census.sum_of_sources(), - )); - } - } - - // 2. Monotonic: a counter that went backwards is a torn read or a + // 1. Monotonic: a counter that went backwards is a torn read or a // word two CPUs are writing, which is what the no-`lock` argument // in `kernel/src/irq_census.rs` rests on being impossible. let mut newest: std::collections::BTreeMap = std::collections::BTreeMap::new(); for census in &lines { if let Some(prev) = newest.get(&census.cpu) { - if census.total < prev.total { + if let Some(name) = SOURCES.iter().zip(prev.by_source.iter().zip(census.by_source)).find_map( + |(name, (&was, now))| (now < was).then_some(name), + ) { return Err(format!( - "cpu{}'s census went backwards, {} then {}: {prev:?} then {census:?}", - census.cpu, prev.total, census.total, + "cpu{}'s `{name}` count went backwards: {prev:?} then {census:?}", + census.cpu, )); } } newest.insert(census.cpu, census.clone()); } - // 3. The machine is real: the boot CPU took interrupts, and so did - // at least one AP — otherwise (4) says nothing. + // 2. The machine is real: the boot CPU took interrupts, and so did + // at least one AP — otherwise (3) says nothing. let cpu0 = newest .get(&0) .ok_or_else(|| format!("no cpu0 in the census: {newest:?}"))?; - if cpu0.total == 0 { + if cpu0.total() == 0 { return Err(format!("cpu0 took no interrupts at all: {cpu0:?}")); } let aps: Vec<&Census> = newest.values().filter(|c| c.cpu != 0).collect(); @@ -2751,11 +2737,11 @@ fn irq_census(capture: &str) -> Result<(), String> { aps.len() )); } - if !aps.iter().any(|c| c.total > 0) { + if !aps.iter().any(|c| c.total() > 0) { return Err(format!("no AP took a single interrupt: {newest:?}")); } - // 4. **The present-state fact this whole track is about.** Every + // 3. **The present-state fact this whole track is about.** Every // message-signalled interrupt is addressed to physical // destination 0 (`drivers::pci`'s `MSG_ADDR`) and the one I/O // APIC pin goes to the BSP, so no AP may have a device count at @@ -2783,17 +2769,17 @@ fn irq_census(capture: &str) -> Result<(), String> { )); } - let share = cpu0.total as f64 - / newest.values().map(|c| c.total).sum::() as f64 + let share = cpu0.total() as f64 + / newest.values().map(Census::total).sum::() as f64 * 100.0; eprintln!( " [irq] {} cpu(s), {} interrupt(s), {delivered} of them device deliveries — \ all on cpu0, which took {share:.1}% of everything", newest.len(), - newest.values().map(|c| c.total).sum::(), + newest.values().map(Census::total).sum::(), ); - // 5. The issuer side: every `tlb` delivery a CPU's census carries + // 4. The issuer side: every `tlb` delivery a CPU's census carries // must be within the issues the `tlb:` line counted — an excess // is a path shooting down uncounted. The lower bound is not // asserted: an issued IPI can be pending on an IF-clear target.