From bc5f36c7bef6cc4b2ba368cd38da93072a3288f3 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 4 Oct 2026 09:30:20 +0200 Subject: [PATCH 1/4] virt_smp's second job wait goes on from the capture its first wait drained `judge_virt_job` began each wait with an empty capture and handed back `boot_log` plus what it drained. A drain takes every line queued in the reader's channel, so the wait for `unmap_touch`'s end could take `test_rs_counters_read`'s end, and even the boot's last word, along with it whenever the harness thread ran late: `virt_smp` then threw that capture away and waited for a line already read, and called the guest stalled. Merge-queue run 37184363691 (#710) shows it: virt_smp's second capture opens at the supervisor's power-off line, so the job had ended and its marker went with the first drain; virt_el1_smp's second capture is empty. On origin/main d47b383cf the pair stalled in 1 of 11 runs on this host at load 37-73 on 14 cores, both tests in that one run. Every caller now threads one capture, started from the boot log, through every wait on its guest. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8 --- tests/toyos.rs | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/tests/toyos.rs b/tests/toyos.rs index 7fac3b67117..e2a82598dae 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -1505,7 +1505,8 @@ fn virt_job(profile: qemu::Profile, job: &str, said: &str) -> Result<(), String> ..Default::default() }, ); - judge_virt_job(&mut qemu, job, said).map(drop) + let mut serial = virt_console(&qemu); + judge_virt_job(&mut qemu, &mut serial, job, said) } /// A `mask-windows` boot's windows: `common::irqcensus::windows`'s verdict, @@ -1524,14 +1525,19 @@ fn mask_windows(capture: &str, cpus: u32) -> Result<(), String> { Ok(()) } +/// Everything the PL011 has carried on `qemu`'s boot: the capture each +/// [`judge_virt_job`] after the first goes on from, since a drain reads past +/// the marker it waited for. +fn virt_console(qemu: &QemuInstance) -> String { + format!("{}\n", qemu.boot_log()) +} + /// Wait for `job`'s end on a guest booted with it, and judge it: it ends with -/// exit 0, having said `said`. Answers everything the PL011 carried. -fn judge_virt_job(qemu: &mut QemuInstance, job: &str, said: &str) -> Result { +/// exit 0, having said `said`. `serial` is everything the PL011 has carried, +/// and takes what this drains. +fn judge_virt_job(qemu: &mut QemuInstance, serial: &mut String, job: &str, said: &str) -> Result<(), String> { let end = format!("===TEST_END {job} "); - let mut rest = String::new(); - let waited = await_marker(qemu, &mut rest, &end, &format!("the job {job} to end")); - let serial = format!("{}\n{rest}", qemu.boot_log()); - if let Err(why) = waited { + if let Err(why) = await_marker(qemu, serial, &end, &format!("the job {job} to end")) { return Err(format!("{why}\nserial:\n{serial}")); } let ended = serial @@ -1545,7 +1551,7 @@ fn judge_virt_job(qemu: &mut QemuInstance, job: &str, said: &str) -> Result Result<(), String> { kernel_features: toyos_build::build::MASK_WINDOWS_KERNEL, ..Default::default() }); - let mut serial = judge_virt_job(&mut qemu, "unmap_touch", UNMAP_TOUCH_SAID)?; + let mut serial = virt_console(&qemu); + judge_virt_job(&mut qemu, &mut serial, "unmap_touch", UNMAP_TOUCH_SAID)?; // To the boot's last word, said after every census and its windows: the drain that took the job's end can stop between the two. await_marker(&mut qemu, &mut serial, power::SHUTTING_DOWN, "the boot's last word")?; mask_windows(&serial, VIRT_CPUS) @@ -1610,7 +1617,8 @@ fn virt_smp(profile: qemu::Profile, conduit: &str, el: u32) -> Result<(), String // Before the job list can reach its `shutdown`: QMP delivers no event // emitted before its client connected. let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)); - let serial = judge_virt_job(&mut qemu, "unmap_touch", UNMAP_TOUCH_SAID)?; + let mut serial = virt_console(&qemu); + judge_virt_job(&mut qemu, &mut serial, "unmap_touch", UNMAP_TOUCH_SAID)?; let psci = serial.lines().find(|l| l.contains("PSCI: ")).unwrap_or_default(); if !psci.contains(&format!(" through {conduit}")) { return Err(format!("PSCI is not said to be reached through {conduit}: {psci:?}\nserial:\n{serial}")); @@ -1632,9 +1640,9 @@ fn virt_smp(profile: qemu::Profile, conduit: &str, el: u32) -> Result<(), String } } eprintln!(" [virt] {VIRT_CPUS} CPUs entered at EL{el}, started through {conduit}, and scheduling"); - let counted = judge_virt_job(&mut qemu, "test_rs_counters_read", COUNTERS_READ_SAID)?; + judge_virt_job(&mut qemu, &mut serial, "test_rs_counters_read", COUNTERS_READ_SAID)?; let (console, calls) = - ended_through_psci(&mut qemu, &mut stop, counted, power::SHUTTING_DOWN, "guest-shutdown", &trace, |_| Vec::new())?; + ended_through_psci(&mut qemu, &mut stop, serial, power::SHUTTING_DOWN, "guest-shutdown", &trace, |_| Vec::new())?; let record = console .lines() .find_map(toyos_quiesce::Record::parse) @@ -1733,7 +1741,8 @@ fn virt_failed_ap_leaves_no_hole(profile: qemu::Profile) -> Result<(), String> { const CPUS: u32 = 4; let mut qemu = boot_virt_smp(BootOptions { profile, smp: CPUS, kernel_params: &["smp-skip-ap"], ..Default::default() }); - let serial = judge_virt_job(&mut qemu, "unmap_touch", UNMAP_TOUCH_SAID)?; + let mut serial = virt_console(&qemu); + judge_virt_job(&mut qemu, &mut serial, "unmap_touch", UNMAP_TOUCH_SAID)?; // The premise, not just a small machine: cpu1 came up and cpu2 did not. for premise in ["SMP: cpu1 mpidr=0x1 online", "SMP: cpu2 mpidr=0x2 did not echo within"] { if !serial.contains(premise) { @@ -1810,7 +1819,8 @@ fn virt_off_names_the_cpus_left_on(profile: qemu::Profile) -> Result<(), String> ..Default::default() }); let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)); - let serial = judge_virt_job(&mut qemu, "unmap_touch", UNMAP_TOUCH_SAID)?; + let mut serial = virt_console(&qemu); + judge_virt_job(&mut qemu, &mut serial, "unmap_touch", UNMAP_TOUCH_SAID)?; let spared = |calls: &[(u64, u64)]| -> Vec { let last = calls.iter().find(|&&(function, _)| function == PSCI_SYSTEM_OFF).map(|&(_, cpu)| cpu); (u64::from(VIRT_CPUS) - 2..u64::from(VIRT_CPUS)).filter(|&cpu| Some(cpu) != last).collect() From 762a524f0d974e5f78143f1936f9a7a7d7b0e2c3 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 4 Oct 2026 10:44:52 +0200 Subject: [PATCH 2/4] Counters: only the answer that completes a round wakes its readers Every CPU's answer posted `ANSWERED`, so a round of N CPUs woke every parked reader N times, each post walking the whole waiter list under the watch's lock with interrupts closed, and each woken reader taking the same lock to re-arm. `test_rs_counters_read`'s sixteen readers on eight CPUs made that lock the machine's: on an 8-vCPU TCG guest on a loaded host a holder descheduled by the host left the rest spinning with interrupts masked, and a read that takes ~0.3 s took 8-26 s, and once 15 s with nothing said, which the harness calls a stall. Registers captured over QMP while a read was slow put four of the eight CPUs at `Lock::lock`'s spin on `counters::ANSWERED` (x19 = its address), two at the kick handler's EOI, one in the post. `Shootdown::serve_if_owed_completing` answers whether, after this serve and a SeqCst fence, every online CPU has served past what this one had: of the CPUs completing a generation, the last one's fence sees every other's store, so exactly the completing answer is guaranteed to post, and an answer that leaves a CPU owing posts nothing. `serve_if_owed`'s return had no other reader and goes. Loom: `counters_round.rs` models two CPUs completing a round at once (reds with the fence removed) and an answer that leaves a CPU owing (reds when every answer completes). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8 --- kernel/loom/tests/counters_round.rs | 31 +++++++++++++++++++++++++++++ kernel/src/counters.rs | 14 ++++++++----- kernel/src/shootdown.rs | 28 +++++++++++++++++++------- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/kernel/loom/tests/counters_round.rs b/kernel/loom/tests/counters_round.rs index 866407c82bb..e4a745e996b 100644 --- a/kernel/loom/tests/counters_round.rs +++ b/kernel/loom/tests/counters_round.rs @@ -68,3 +68,34 @@ fn a_cpu_read_as_answered_is_read_with_its_answer() { }); assert!(READ.load(SeqCst), "no interleaving found the round answered, so the assertion never ran"); } + +/// Two CPUs answering one round at once: whichever of them completes it says +/// so, or its readers park to the bound with every answer in. +#[test] +fn the_answer_that_completes_a_round_says_so() { + let mut builder = loom::model::Builder::new(); + builder.preemption_bound = Some(3); + builder.check(|| { + let rounds = Arc::new(Shootdown::new()); + rounds.issue(); + let cpu1 = { + let rounds = rounds.clone(); + loom::thread::spawn(move || rounds.serve_if_owed_completing(1, 2, || ())) + }; + let cpu0 = rounds.serve_if_owed_completing(0, 2, || ()); + let cpu1 = cpu1.join().unwrap(); + assert!(cpu0 || cpu1, "both CPUs answered the round and neither said it completed it"); + }); +} + +/// An answer with another CPU still owing wakes nobody: a post per answer +/// wakes every reader once per CPU. +#[test] +fn an_answer_that_leaves_a_cpu_owing_does_not_complete_the_round() { + loom::model(|| { + let rounds = Shootdown::new(); + rounds.issue(); + assert!(!rounds.serve_if_owed_completing(0, 2, || ()), "cpu 0 completed a round cpu 1 still owes"); + assert!(rounds.serve_if_owed_completing(1, 2, || ()), "cpu 1's answer completed the round and did not say so"); + }); +} diff --git a/kernel/src/counters.rs b/kernel/src/counters.rs index b0a43211113..d647c98ad86 100644 --- a/kernel/src/counters.rs +++ b/kernel/src/counters.rs @@ -5,7 +5,10 @@ //! round ([`Shootdown`]'s protocol): the reader issues a generation, answers //! for its own CPU, kicks every other, and parks until each has answered or //! the round's [`ANSWER`] bound has passed. A CPU answers from its kick -//! handler ([`serve_here`]) wherever it was; one whose interrupts stay closed +//! handler ([`serve_here`]) wherever it was, and only the answer that +//! completes a round wakes its readers: a post per answer would wake every +//! reader once per CPU, under the lock each answer and each reader takes with +//! interrupts closed. A CPU whose interrupts stay closed //! past the bound is reported stale with the last block it published, never //! waited for without end. A read within [`JOIN`] of the last round's issue //! waits on that round, so no reader makes a CPU take more than one kick per @@ -106,8 +109,8 @@ pub fn serve_here() { /// This CPU's answer, after kicking every other CPU when `kick` says so. fn answer(kick: bool) { - let served = { - // One writer per block, and one CPU both skipped by the kick and answered for. + let completed = { + // One writer per block, one serve at a time per CPU, and one CPU both skipped by the kick and answered for. let _closed = IrqGuard::close(); if kick { irqchip::kick_all_but_self(); @@ -117,9 +120,10 @@ fn answer(kick: bool) { if deaf::DEAF.load(core::sync::atomic::Ordering::Relaxed) == me { return; } - ROUNDS.serve_if_owed(me, || BLOCKS[me].publish(sample(me))) + let cpus = crate::smp::cpu_count() as usize; + ROUNDS.serve_if_owed_completing(me, cpus, || BLOCKS[me].publish(sample(me))) }; - if served { + if completed { ANSWERED.post_in_place(); } } diff --git a/kernel/src/shootdown.rs b/kernel/src/shootdown.rs index 4443e820f28..dd0e003322d 100644 --- a/kernel/src/shootdown.rs +++ b/kernel/src/shootdown.rs @@ -5,10 +5,10 @@ //! and an initiator that sees a generation served sees everything that answer wrote. #[cfg(not(feature = "loom"))] -use core::sync::atomic::{AtomicU64, Ordering}; +use core::sync::atomic::{fence, AtomicU64, Ordering}; #[cfg(feature = "loom")] -use loom::sync::atomic::{AtomicU64, Ordering}; +use loom::sync::atomic::{fence, AtomicU64, Ordering}; /// Matches `sched::MAX_CPUS`. pub const MAX_CPUS: usize = 8; @@ -82,13 +82,27 @@ impl Shootdown { self.flushed[cpu].load(SERVED) >= generation.0 } - /// [`serve`](Self::serve) for a CPU not taking the interrupt; whether it served. - pub fn serve_if_owed(&self, cpu: usize, flush: impl FnOnce()) -> bool { - let owed = self.owes(cpu); - if owed { + /// [`serve`](Self::serve) for a CPU not taking the interrupt. + pub fn serve_if_owed(&self, cpu: usize, flush: impl FnOnce()) { + if self.owes(cpu) { self.serve(cpu, flush); } - owed + } + + /// [`serve_if_owed`](Self::serve_if_owed) for a round whose waiters park: + /// whether it served, and after it each of the first `cpus` CPUs has + /// served past what `cpu` had. Of the CPUs completing a generation the last + /// answers `true`, so its waiters are woken once a round and not once a + /// CPU. A serve on `cpu` must not nest inside this one. + pub fn serve_if_owed_completing(&self, cpu: usize, cpus: usize, flush: impl FnOnce()) -> bool { + if !self.owes(cpu) { + return false; + } + let before = self.flushed[cpu].load(Ordering::Relaxed); + self.serve(cpu, flush); + // SeqCst: of CPUs serving at once, the one whose fence is last sees every other's store. + fence(Ordering::SeqCst); + self.flushed[..cpus].iter().all(|flushed| flushed.load(Ordering::Relaxed) > before) } /// One turn of an initiator's wait for `cpu`: answer first, then ask. From 34d0d08cc79bedb8cdf5399036f6316d5b3acf84 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 4 Oct 2026 11:16:13 +0200 Subject: [PATCH 3/4] Revert "Counters: only the answer that completes a round wakes its readers" This reverts commit 762a524f0. It was built on registers captured during one slow read, and it changed nothing it could be measured by: over 270 guests of `virt_smp` and `virt_el1_smp` six at a time (fix, base, fix; host 1-minute load 18-60 on 14 cores) the kicks taken during `test_rs_counters_read` were median 116/122/118, its guest-clock span median 0.189/0.195/0.200 s, and no arm reddened. The silent read it was aimed at is filed instead, with that evidence: issues/kernel/a-counters-read-under-host-load-can-go-silent-for-15-s.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8 --- ...-under-host-load-can-go-silent-for-15-s.md | 42 +++++++++++++++++++ kernel/loom/tests/counters_round.rs | 31 -------------- kernel/src/counters.rs | 14 +++---- kernel/src/shootdown.rs | 28 ++++--------- 4 files changed, 54 insertions(+), 61 deletions(-) create mode 100644 issues/kernel/a-counters-read-under-host-load-can-go-silent-for-15-s.md diff --git a/issues/kernel/a-counters-read-under-host-load-can-go-silent-for-15-s.md b/issues/kernel/a-counters-read-under-host-load-can-go-silent-for-15-s.md new file mode 100644 index 00000000000..603fbfe548a --- /dev/null +++ b/issues/kernel/a-counters-read-under-host-load-can-go-silent-for-15-s.md @@ -0,0 +1,42 @@ +--- +status: open +kind: defect +opened: 2026-10-04 +--- + +# A counters read under host load can go silent for 15 s on an 8-CPU `virt` guest + +`test_rs_counters_read` in `tests/virtsmpcase` normally ends about 0.2 s of +guest clock after `unmap_touch` does (median 0.17-0.20 s over 570 guests, max +1.46 s). Under host load it sometimes takes seconds, and once it went silent +long enough for the harness's 15 s quiet bound to call the guest stalled. + +The silent one: `virt_el1_smp`, 8 vCPUs under TCG, on a 14-core host at +1-minute load 38-53 while the primary checkout was building the stage-2 +compiler; the branch `wt/toyos-counterstall` at `bc5f36c7b`, whose harness +keeps every line the guest said. `test_rs_counters_read` started at 4.031, +spawned at 4.044, two of its threads exited (4.174 and 4.576), and then the +console carried nothing until the harness gave up 15 s later. No CPU printed +`sched: cpu=` again, though each last printed one at 2.17-2.22 s and prints +again on its first idle trip 10 s on (`scheduler::log_health`): no CPU went +idle, or the console stopped. No register capture of that guest exists. One +in 30 guests of that loop; none in the 1724 guests that followed on the same +host at load 15-60. + +The slow ones, with registers: a probe that captured `info registers -a` over +QMP whenever the read had not ended 3 s after `unmap_touch` caught one (span +3.68 s, `virt_el1_smp`, load 38) in 174 guests. Every CPU was at EL1 with +DAIF masked; four (cpu0, 3, 5, 7) were in `Lock::lock`'s ticket spin on +`counters::ANSWERED`'s waiter list (x19 its address in the shipping kernel's +symbols), two at the kick handler's `ICC_EOIR1_EL1` write, one inside the +post. 500 ms later all were back in `driver::pass`, and the read finished. +Earlier slow reads without registers spanned 7, 15 and 26 s. + +Waking a round's readers once, from the answer that completes it, instead of +once per answering CPU (`762a524f0`, reverted in the next commit) changed +neither the kicks taken during the read (median 116/122/118, fix/base/fix) +nor its span, so the waiter-list contention is not shown to be the cause. + +Exit: the cause of the silence is named from a capture of a silent guest +(registers over QMP before anything else touches it), and fixed with the +evidence, or shown to be the host stopping the guest. diff --git a/kernel/loom/tests/counters_round.rs b/kernel/loom/tests/counters_round.rs index e4a745e996b..866407c82bb 100644 --- a/kernel/loom/tests/counters_round.rs +++ b/kernel/loom/tests/counters_round.rs @@ -68,34 +68,3 @@ fn a_cpu_read_as_answered_is_read_with_its_answer() { }); assert!(READ.load(SeqCst), "no interleaving found the round answered, so the assertion never ran"); } - -/// Two CPUs answering one round at once: whichever of them completes it says -/// so, or its readers park to the bound with every answer in. -#[test] -fn the_answer_that_completes_a_round_says_so() { - let mut builder = loom::model::Builder::new(); - builder.preemption_bound = Some(3); - builder.check(|| { - let rounds = Arc::new(Shootdown::new()); - rounds.issue(); - let cpu1 = { - let rounds = rounds.clone(); - loom::thread::spawn(move || rounds.serve_if_owed_completing(1, 2, || ())) - }; - let cpu0 = rounds.serve_if_owed_completing(0, 2, || ()); - let cpu1 = cpu1.join().unwrap(); - assert!(cpu0 || cpu1, "both CPUs answered the round and neither said it completed it"); - }); -} - -/// An answer with another CPU still owing wakes nobody: a post per answer -/// wakes every reader once per CPU. -#[test] -fn an_answer_that_leaves_a_cpu_owing_does_not_complete_the_round() { - loom::model(|| { - let rounds = Shootdown::new(); - rounds.issue(); - assert!(!rounds.serve_if_owed_completing(0, 2, || ()), "cpu 0 completed a round cpu 1 still owes"); - assert!(rounds.serve_if_owed_completing(1, 2, || ()), "cpu 1's answer completed the round and did not say so"); - }); -} diff --git a/kernel/src/counters.rs b/kernel/src/counters.rs index d647c98ad86..b0a43211113 100644 --- a/kernel/src/counters.rs +++ b/kernel/src/counters.rs @@ -5,10 +5,7 @@ //! round ([`Shootdown`]'s protocol): the reader issues a generation, answers //! for its own CPU, kicks every other, and parks until each has answered or //! the round's [`ANSWER`] bound has passed. A CPU answers from its kick -//! handler ([`serve_here`]) wherever it was, and only the answer that -//! completes a round wakes its readers: a post per answer would wake every -//! reader once per CPU, under the lock each answer and each reader takes with -//! interrupts closed. A CPU whose interrupts stay closed +//! handler ([`serve_here`]) wherever it was; one whose interrupts stay closed //! past the bound is reported stale with the last block it published, never //! waited for without end. A read within [`JOIN`] of the last round's issue //! waits on that round, so no reader makes a CPU take more than one kick per @@ -109,8 +106,8 @@ pub fn serve_here() { /// This CPU's answer, after kicking every other CPU when `kick` says so. fn answer(kick: bool) { - let completed = { - // One writer per block, one serve at a time per CPU, and one CPU both skipped by the kick and answered for. + let served = { + // One writer per block, and one CPU both skipped by the kick and answered for. let _closed = IrqGuard::close(); if kick { irqchip::kick_all_but_self(); @@ -120,10 +117,9 @@ fn answer(kick: bool) { if deaf::DEAF.load(core::sync::atomic::Ordering::Relaxed) == me { return; } - let cpus = crate::smp::cpu_count() as usize; - ROUNDS.serve_if_owed_completing(me, cpus, || BLOCKS[me].publish(sample(me))) + ROUNDS.serve_if_owed(me, || BLOCKS[me].publish(sample(me))) }; - if completed { + if served { ANSWERED.post_in_place(); } } diff --git a/kernel/src/shootdown.rs b/kernel/src/shootdown.rs index dd0e003322d..4443e820f28 100644 --- a/kernel/src/shootdown.rs +++ b/kernel/src/shootdown.rs @@ -5,10 +5,10 @@ //! and an initiator that sees a generation served sees everything that answer wrote. #[cfg(not(feature = "loom"))] -use core::sync::atomic::{fence, AtomicU64, Ordering}; +use core::sync::atomic::{AtomicU64, Ordering}; #[cfg(feature = "loom")] -use loom::sync::atomic::{fence, AtomicU64, Ordering}; +use loom::sync::atomic::{AtomicU64, Ordering}; /// Matches `sched::MAX_CPUS`. pub const MAX_CPUS: usize = 8; @@ -82,27 +82,13 @@ impl Shootdown { self.flushed[cpu].load(SERVED) >= generation.0 } - /// [`serve`](Self::serve) for a CPU not taking the interrupt. - pub fn serve_if_owed(&self, cpu: usize, flush: impl FnOnce()) { - if self.owes(cpu) { + /// [`serve`](Self::serve) for a CPU not taking the interrupt; whether it served. + pub fn serve_if_owed(&self, cpu: usize, flush: impl FnOnce()) -> bool { + let owed = self.owes(cpu); + if owed { self.serve(cpu, flush); } - } - - /// [`serve_if_owed`](Self::serve_if_owed) for a round whose waiters park: - /// whether it served, and after it each of the first `cpus` CPUs has - /// served past what `cpu` had. Of the CPUs completing a generation the last - /// answers `true`, so its waiters are woken once a round and not once a - /// CPU. A serve on `cpu` must not nest inside this one. - pub fn serve_if_owed_completing(&self, cpu: usize, cpus: usize, flush: impl FnOnce()) -> bool { - if !self.owes(cpu) { - return false; - } - let before = self.flushed[cpu].load(Ordering::Relaxed); - self.serve(cpu, flush); - // SeqCst: of CPUs serving at once, the one whose fence is last sees every other's store. - fence(Ordering::SeqCst); - self.flushed[..cpus].iter().all(|flushed| flushed.load(Ordering::Relaxed) > before) + owed } /// One turn of an initiator's wait for `cpu`: answer first, then ask. From f6bff0f3264baa5531995aff93ba61f411f70729 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 4 Oct 2026 11:46:41 +0200 Subject: [PATCH 4/4] Issue: cite the posted register-capture probes The slow-read probe the issue names lived only in a scratchpad; it is posted on #719 with the stall capture, and the issue cites that comment. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8 --- .../a-counters-read-under-host-load-can-go-silent-for-15-s.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/issues/kernel/a-counters-read-under-host-load-can-go-silent-for-15-s.md b/issues/kernel/a-counters-read-under-host-load-can-go-silent-for-15-s.md index 603fbfe548a..9fe7bf58773 100644 --- a/issues/kernel/a-counters-read-under-host-load-can-go-silent-for-15-s.md +++ b/issues/kernel/a-counters-read-under-host-load-can-go-silent-for-15-s.md @@ -24,7 +24,9 @@ in 30 guests of that loop; none in the 1724 guests that followed on the same host at load 15-60. The slow ones, with registers: a probe that captured `info registers -a` over -QMP whenever the read had not ended 3 s after `unmap_touch` caught one (span +QMP whenever the read had not ended 3 s after `unmap_touch` (`debug-slow.patch` +in https://github.com/ToyOSOrg/ToyOS/pull/719#issuecomment-5978643928, which +also carries `debug-regs.patch`, the capture of a stalled one) caught one (span 3.68 s, `virt_el1_smp`, load 38) in 174 guests. Every CPU was at EL1 with DAIF masked; four (cpu0, 3, 5, 7) were in `Lock::lock`'s ticket spin on `counters::ANSWERED`'s waiter list (x19 its address in the shipping kernel's