From c2289e4ba4132aa3fb2afaadba3136bf3999b199 Mon Sep 17 00:00:00 2001 From: Jordan Blake Date: Wed, 9 Sep 2026 01:30:16 -0500 Subject: [PATCH] os_sleep: process due sleep timers one at a time to stop stranding parked threads (#195) ProcessSleepTimers popped every due timer into a private vector and then resumed the sleepers in a loop. OSResumeThread re-enters SelectThread, which can switch fibers away mid-loop, so the timers still in that vector were gone from gSleepTimers while their threads stayed parked (Ready, suspended, no timer). The reconciler healed them 100ms later and the stale-timer drop fired when the original fiber eventually resumed. Pop one due timer at a time straight from the shared table instead, so any timer not yet processed stays visible to every other pump while this call is switched away. Co-authored-by: jordanblakepp Co-authored-by: Claude Fable 5.1 --- runtime/src/hle/os/os_sleep.cpp | 42 +++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/runtime/src/hle/os/os_sleep.cpp b/runtime/src/hle/os/os_sleep.cpp index 5938dda..5a66c02 100644 --- a/runtime/src/hle/os/os_sleep.cpp +++ b/runtime/src/hle/os/os_sleep.cpp @@ -78,22 +78,38 @@ bool ProcessSleepTimers(CpuContext* cpu) { using Clock = std::chrono::steady_clock; - std::vector dueTimers; + // Pop and process ONE due timer at a time, straight from the shared table. Resuming a + // sleeper re-enters the scheduler (OSResumeThread -> SelectThread) and can switch fibers + // away from this call. Timers that had already been popped into a private list would then + // sit on the suspended fiber's stack with their threads parked and no entry in the table: + // exactly the "park-shaped with no pending wake timer" strand the reconciler below heals + // 100ms late, followed by a "sleep-timer stale" drop when this fiber finally resumes. + // Leaving unprocessed timers in the table keeps them visible to every other pump (idle + // loop, other threads' SelectThread) while this one is switched away. + bool processedAny = false; + constexpr size_t kMaxTimersPerCall = 64; + size_t processedCount = 0; const auto now = Clock::now(); - { - std::lock_guard lock(gSleepTimerMutex); - auto it = gSleepTimers.begin(); - while (it != gSleepTimers.end()) { - if (it->deadline > now) { - ++it; - continue; + while (processedCount < kMaxTimersPerCall) { + SleepTimerEntry timer{0, {}}; + bool found = false; + { + std::lock_guard lock(gSleepTimerMutex); + for (auto it = gSleepTimers.begin(); it != gSleepTimers.end(); ++it) { + if (it->deadline <= now) { + timer = *it; + gSleepTimers.erase(it); + found = true; + break; + } } - dueTimers.push_back(*it); - it = gSleepTimers.erase(it); } - } + if (!found) { + break; + } + ++processedCount; + processedAny = true; - for (const SleepTimerEntry& timer : dueTimers) { const uint32_t threadPtr = timer.threadPtr; if (threadPtr == 0 || !Memory::Contains(threadPtr + kThreadSuspendOffset, sizeof(uint32_t))) { @@ -219,7 +235,7 @@ bool ProcessSleepTimers(CpuContext* cpu) } } - return !dueTimers.empty(); + return processedAny; } } // namespace OsHleInternal