page: the provider's stop-the-world (GuestStop) in the shim: guest threads park at safepoints and syscall returns while Mono collects

tests/unity-dotnet/embed in Chromium: 3 of 3 runs every line as natively (0 bad of 640).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_d2453757b8a84fdbb4ee7c9e093141df
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-07 22:26:03 -04:00
1 parent 8663dfee5e
commit 171b777265
3 files changed
+206 -6

No files matched your search

+5 -6
View File
@@ -2682,12 +2682,11 @@ export class Gex {
provider_guest_stack_end: (sp) => e.gex_libprov_guest_stack_end(sp),
provider_abi: (which) => e.gex_libprov_abi(which),
provider_table_size: (base) => e.gex_libprov_table_size(base),
// Stopping the guest's threads around the runtime's collection: not yet in the page
// (each guest thread is a Worker; the stop needs the shim's safepoints).
provider_thread_register: () => {},
provider_park: () => {},
provider_stop_guest: () => {},
provider_start_guest: () => {},
// Stopping the guest's threads around the runtime's collection (cgen/libprov.rs).
provider_thread_register: (inGuest, stop) => e.gex_libprov_thread_register(inGuest, stop),
provider_park: () => e.gex_libprov_park(),
provider_stop_guest: () => e.gex_libprov_stop(),
provider_start_guest: () => e.gex_libprov_start(),
// The runtime's wasm JIT: not yet in the page (B2); it runs interpreted.
provider_jit_reserve: () => 0n,
provider_jit_instantiate: () => -1,
+184
View File
@@ -362,3 +362,187 @@ fn elf_fini(bytes: &[u8]) -> Vec<u64> {
}
out
}
// ---------------------------------------------------------------------------
// Stopping the guest's threads for the provider's collector (native
// `libprov::GuestStop`): each thread registers the provider's "in guest
// code" word for it and the provider's stop flag; threads running guest code
// stop at their next safepoint or syscall while a collection runs, a thread
// inside a syscall counts as stopped. Every Worker shares these statics (the
// shim's memory is the process's).
// ---------------------------------------------------------------------------
#[derive(Default)]
struct StopState {
threads: HashMap<u32, u64>,
stop_word: u64,
parked: std::collections::HashSet<u32>,
in_kernel: std::collections::HashSet<u32>,
stopper: Option<u32>,
}
static STOPPING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// Some thread registered (the syscall path's fast test).
static ANY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// Whether any thread of this process registered with a provider.
pub(crate) fn any_registered() -> bool {
ANY.load(Ordering::Relaxed)
}
static STOP: Mutex<Option<StopState>> = Mutex::new(None);
/// Bumped on every change a waiter could be waiting for (park, start).
static STOP_EPOCH: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
fn stop_state<R>(f: impl FnOnce(&mut StopState) -> R) -> R {
f(lock(&STOP).get_or_insert_with(Default::default))
}
fn word(addr: u64) -> u32 {
let mem = crate::guest_bytes();
mem.get(addr as usize..addr as usize + 4).map_or(0, |b| u32::from_le_bytes(b.try_into().unwrap()))
}
fn store_word(addr: u64, v: u32) {
std::sync::atomic::fence(Ordering::SeqCst);
if let Some(b) = crate::guest_bytes().get_mut(addr as usize..addr as usize + 4) {
b.copy_from_slice(&v.to_le_bytes());
}
std::sync::atomic::fence(Ordering::SeqCst);
}
fn bump() {
STOP_EPOCH.fetch_add(1, Ordering::SeqCst);
#[cfg(feature = "shared-memory")]
unsafe {
crate::warch::memory_atomic_notify(STOP_EPOCH.as_ptr() as *mut i32, u32::MAX);
}
}
/// Wait (at most `ns`) for the next bump after `seen`.
fn wait_epoch(seen: u32, ns: i64) {
#[cfg(feature = "shared-memory")]
unsafe {
crate::warch::memory_atomic_wait32(STOP_EPOCH.as_ptr() as *mut i32, seen as i32, ns);
}
#[cfg(not(feature = "shared-memory"))]
let _ = (seen, ns);
}
fn this_tid() -> u32 {
crate::with_host(|h| h.threads[crate::cur()].kstate.tid) as u32
}
/// Whether a collection is stopping the guest's threads.
pub(crate) fn stop_active() -> bool {
STOPPING.load(Ordering::Acquire)
}
/// At a safepoint or a syscall boundary in guest code (or from the
/// provider, `forced`): park while a collection runs.
pub(crate) fn park_if_stopping(tid: u32, forced: bool) {
if !STOPPING.load(Ordering::Acquire) {
return;
}
let parks = stop_state(|st| {
let Some(&w) = st.threads.get(&tid) else { return false };
if st.stopper == Some(tid) || (!forced && word(w) == 0) {
return false;
}
st.parked.insert(tid);
true
});
if !parks {
return;
}
bump();
while STOPPING.load(Ordering::Acquire) {
let seen = STOP_EPOCH.load(Ordering::SeqCst);
if !STOPPING.load(Ordering::Acquire) {
break;
}
wait_epoch(seen, 1_000_000);
}
stop_state(|st| st.parked.remove(&tid));
}
/// A syscall made from guest code: counts as stopped until it returns.
pub(crate) fn syscall_enter(tid: u32) {
if lock(&STOP).as_ref().is_some_and(|st| st.threads.contains_key(&tid)) {
stop_state(|st| st.in_kernel.insert(tid));
bump();
}
}
/// Back to guest code (a syscall returned, or a safepoint): park if a
/// collection is on.
pub(crate) fn guest_again(tid: u32) {
let was = lock(&STOP).as_mut().is_some_and(|st| st.in_kernel.remove(&tid));
if was || STOPPING.load(Ordering::Acquire) {
park_if_stopping(tid, false);
}
}
/// `gex.provider_thread_register(in_guest, stop)`.
#[no_mangle]
pub extern "C" fn gex_libprov_thread_register(in_guest: u64, stop: u64) {
let tid = this_tid();
ANY.store(true, Ordering::Relaxed);
stop_state(|st| {
st.threads.insert(tid, in_guest);
st.stop_word = stop;
});
}
/// `gex.provider_park()`.
#[no_mangle]
pub extern "C" fn gex_libprov_park() {
park_if_stopping(this_tid(), true);
}
/// `gex.provider_stop_guest()`: stop every other registered thread that is
/// in guest code (10 s at most, then on without the stragglers, said).
#[no_mangle]
pub extern "C" fn gex_libprov_stop() {
let me = this_tid();
let stop_word = stop_state(|st| {
st.stopper = Some(me);
st.stop_word
});
if stop_word != 0 {
store_word(stop_word, 1);
}
STOPPING.store(true, Ordering::SeqCst);
let deadline = crate::now_ns() + 10_000_000_000;
loop {
let seen = STOP_EPOCH.load(Ordering::SeqCst);
let waiting: Vec<u32> = stop_state(|st| {
st.threads
.iter()
.filter(|(&t, &w)| t != me && word(w) != 0 && !st.parked.contains(&t) && !st.in_kernel.contains(&t))
.map(|(&t, _)| t)
.collect()
});
if waiting.is_empty() {
return;
}
if crate::now_ns() > deadline {
crate::warn_line(&format!("gex: provider stop-the-world: threads {waiting:?} never reached a safepoint; going on without them\n"));
return;
}
wait_epoch(seen, 2_000_000);
}
}
/// `gex.provider_start_guest()`.
#[no_mangle]
pub extern "C" fn gex_libprov_start() {
let stop_word = stop_state(|st| {
st.stopper = None;
st.stop_word
});
if stop_word != 0 {
store_word(stop_word, 0);
}
STOPPING.store(false, Ordering::SeqCst);
bump();
}
+17
View File
@@ -5880,6 +5880,13 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
IN_PROVIDER.with(|c| c.set(provider));
((pc as u64) & !PROVIDER_SYSCALL) as i64
};
// A guest thread in a syscall counts as stopped for a provider's
// collection (cgen/libprov.rs); it parks on its way back if one runs.
let stop_tracked = !IN_PROVIDER.with(|c| c.get()) && cgen::libprov::any_registered();
let tracked_tid = if stop_tracked { with_host(|h| h.threads[cur()].kstate.tid) as u32 } else { 0 };
if stop_tracked {
cgen::libprov::syscall_enter(tracked_tid);
}
// Which syscall is running, so the allocator can name who was asking (wasm
// has no backtrace). See `HEAP_BY_CALL`.
{
@@ -6748,6 +6755,9 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
// A provider's priority-inheritance lock still held (`PI_RETRY` set the
// thread to make the call again): it has no instruction to go back to, so
// the call is made again here, outside the host lock.
if stop == 0 && stop_tracked {
cgen::libprov::guest_again(tracked_tid);
}
if stop != 0 && IN_PROVIDER.with(|c| c.get()) && resume() == Some(pc as u64) {
set_resume_none();
set_resume_ctx_none();
@@ -10460,6 +10470,13 @@ pub extern "C" fn gex_safepoint(ctx: i32, pc: i64) {
// A Windows process's clocks in KUSER_SHARED_DATA, which a guest polls
// without a syscall: a loop that spins on them passes here.
nt::tick();
// A stateful provider's collection stops guest threads here (its
// registers are in the context at a safepoint), and a thread that came
// back from a syscall through JS is running guest code again.
if cgen::libprov::any_registered() {
let tid = with_host(|h| h.threads[cur()].kstate.tid) as u32;
cgen::libprov::guest_again(tid);
}
// A loop that has left an exception dispatched nested (nt::exc_escape).
if nt::exc_escape(ctx, pc as u64) {
set_resume_none();