mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 12:48:39 +08:00
[Kernel] Phase 4: rwlocks block in place; park-state diagnostics
Fourth phase of the 1:1 threading rework. pthread rwlocks were the last primitive on the cooperative path; with them converted, nothing calls RequestCurrentThreadBlock any more, so the continuation/wake-key machinery (including the nested-callback Thread.Sleep(1) spin the primary thread used) is unreachable by construction — the main thread now parks in place inside HLE calls like every other thread. - PthreadRwlockLockCore: drop the RequestCurrentThreadBlock branches bolted in front of the existing Monitor.Wait loops; writers and readers park in place on SyncRoot (sliced for teardown). Removed RwlockWaiter, TryAcquireBlockedRwlock, the per-lock WakeKey, and the unlock's WakeBlockedThreads dispatch. - Diagnostics: in-place-parked threads previously reported state=Running block=none, blinding the stall watchdog. GuestThreadBlocking now tracks what each parked thread waits on (recorded only on the contended slow path); the stall dump's per-thread block= field falls back to it, and a new "Stall in-place blocks" line lists every parked thread — covering the primary thread, which has no _guestThreads entry. The new diagnostics immediately paid for themselves on Silent Hill f: all 38 parked threads are workers (cond_wait/WaitEqueue); the main thread is NOT parked in any primitive — it spins in guest code calling the scePthreadGetspecific leaf (leaf fast-path calls are neither counted nor traced), i.e. it polls a TLS slot that is never filled. That relocates the game's wall from "scheduler wedge" to "missing produced value," to be chased in the bootstrap-cohort investigation. Verified (Metal, headless): void 5.6M imports, Hades 34.4M, Dead Cells 10.1M, Dream Sara, Astro 6.8M (rwlock-heavy: zero writer-conflict reports), Silent Hill / Lunar stable — zero unrecovered crashes across all eight. All 351 Libs tests pass.
This commit is contained in:
@@ -6033,6 +6033,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Stall stack: [rsp]=0x{value:X16} [rsp+8]=0x{value2:X16}");
|
||||
}
|
||||
|
||||
// Threads blocked in place on host primitives (incl. the primary
|
||||
// thread, which has no _guestThreads entry) and what they wait on.
|
||||
var parked = GuestThreadBlocking.SnapshotBlockDescriptions();
|
||||
if (parked.Length != 0)
|
||||
{
|
||||
var builder = new System.Text.StringBuilder(64 + parked.Length * 40);
|
||||
builder.Append("[LOADER][ERROR] Stall in-place blocks:");
|
||||
foreach (var entry in parked)
|
||||
{
|
||||
builder.Append($" 0x{entry.Key:X16}={entry.Value}");
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(builder.ToString());
|
||||
}
|
||||
|
||||
var threads = SnapshotGuestThreads();
|
||||
if (threads.Length != 0)
|
||||
{
|
||||
@@ -6053,12 +6068,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
hostContextText = $" host_tid={hostThreadId} host_ctx=unavailable";
|
||||
}
|
||||
|
||||
// Threads parked in place on a host primitive stay state=Running;
|
||||
// GuestThreadBlocking records what they are parked on.
|
||||
var blockText = thread.BlockReason
|
||||
?? GuestThreadBlocking.DescribeBlock(thread.ThreadHandle)
|
||||
?? "none";
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Stall guest-thread: handle=0x{thread.ThreadHandle:X16} name='{thread.Name}' " +
|
||||
$"state={thread.State} imports={Interlocked.Read(ref thread.ImportCount)} " +
|
||||
$"nid={Volatile.Read(ref thread.LastImportNid) ?? "none"} ret=0x{Volatile.Read(ref thread.LastReturnRip):X16} " +
|
||||
$"rdi=0x{Volatile.Read(ref thread.LastImportRdi):X16} rsi=0x{Volatile.Read(ref thread.LastImportRsi):X16} " +
|
||||
$"rdx=0x{Volatile.Read(ref thread.LastImportRdx):X16} block={thread.BlockReason ?? "none"}{hostContextText}");
|
||||
$"rdx=0x{Volatile.Read(ref thread.LastImportRdx):X16} block={blockText}{hostContextText}");
|
||||
logged++;
|
||||
if (logged >= 48 && threads.Length > logged)
|
||||
{
|
||||
|
||||
@@ -22,9 +22,39 @@ public static class GuestThreadBlocking
|
||||
|
||||
private static volatile bool _shutdownRequested;
|
||||
|
||||
// Guest thread handle -> what it is parked on. Populated only while a
|
||||
// thread is blocked (the slow path), read by the stall watchdog so
|
||||
// in-place-blocked threads are not reported as opaque "Running" threads.
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, string> _blockDescriptions = new();
|
||||
|
||||
/// <summary>True once emulator teardown has begun; parked guest threads unwind.</summary>
|
||||
public static bool ShutdownRequested => _shutdownRequested;
|
||||
|
||||
/// <summary>Called by the execution backend when guest execution is being torn down.</summary>
|
||||
public static void RequestShutdown() => _shutdownRequested = true;
|
||||
|
||||
/// <summary>Records what the given guest thread is about to park on (diagnostics only).</summary>
|
||||
public static void NoteBlocked(ulong guestThreadHandle, string description)
|
||||
{
|
||||
if (guestThreadHandle != 0)
|
||||
{
|
||||
_blockDescriptions[guestThreadHandle] = description;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clears the parked-state note recorded by <see cref="NoteBlocked"/>.</summary>
|
||||
public static void NoteUnblocked(ulong guestThreadHandle)
|
||||
{
|
||||
if (guestThreadHandle != 0)
|
||||
{
|
||||
_blockDescriptions.TryRemove(guestThreadHandle, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>What the thread is parked on, or null if it is not parked in place.</summary>
|
||||
public static string? DescribeBlock(ulong guestThreadHandle) =>
|
||||
_blockDescriptions.TryGetValue(guestThreadHandle, out var description) ? description : null;
|
||||
|
||||
/// <summary>All currently parked threads (diagnostics; covers the primary thread too).</summary>
|
||||
public static KeyValuePair<ulong, string>[] SnapshotBlockDescriptions() => _blockDescriptions.ToArray();
|
||||
}
|
||||
|
||||
@@ -249,6 +249,7 @@ public static class KernelEventFlagCompatExports
|
||||
// event flags: OR/AND over the bit pattern, optional clear).
|
||||
state.WaitingThreads++;
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{returnRip:X16}");
|
||||
GuestThreadBlocking.NoteBlocked(GuestThreadExecution.CurrentGuestThreadHandle, "sceKernelWaitEventFlag");
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
@@ -282,6 +283,7 @@ public static class KernelEventFlagCompatExports
|
||||
finally
|
||||
{
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
GuestThreadBlocking.NoteUnblocked(GuestThreadExecution.CurrentGuestThreadHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,29 +421,37 @@ public static class KernelEventQueueCompatExports
|
||||
}
|
||||
|
||||
TraceEventQueue(ctx, "wait-block", handle);
|
||||
lock (_eventQueueGate)
|
||||
GuestThreadBlocking.NoteBlocked(GuestThreadExecution.CurrentGuestThreadHandle, "sceKernelWaitEqueue");
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
if ((_pendingEvents.TryGetValue(handle, out var queue) && queue.Count != 0) ||
|
||||
!_eventQueues.Contains(handle) ||
|
||||
GuestThreadBlocking.ShutdownRequested)
|
||||
while (true)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if ((_pendingEvents.TryGetValue(handle, out var queue) && queue.Count != 0) ||
|
||||
!_eventQueues.Contains(handle) ||
|
||||
GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (timeoutAddress != 0 && remaining <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (timeoutAddress != 0 && remaining <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var slice = timeoutAddress == 0
|
||||
? GuestThreadBlocking.WaitSliceMilliseconds
|
||||
: (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
_ = Monitor.Wait(_eventQueueGate, slice);
|
||||
var slice = timeoutAddress == 0
|
||||
? GuestThreadBlocking.WaitSliceMilliseconds
|
||||
: (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
_ = Monitor.Wait(_eventQueueGate, slice);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadBlocking.NoteUnblocked(GuestThreadExecution.CurrentGuestThreadHandle);
|
||||
}
|
||||
|
||||
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
|
||||
@@ -691,6 +691,7 @@ public static class KernelPthreadCompatExports
|
||||
// parks, so an unlock's PulseAll cannot be missed. Waits are
|
||||
// sliced only so teardown can unwind parked threads.
|
||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_mutex_lock");
|
||||
state.WaiterCount++;
|
||||
try
|
||||
{
|
||||
@@ -711,6 +712,7 @@ public static class KernelPthreadCompatExports
|
||||
finally
|
||||
{
|
||||
state.WaiterCount--;
|
||||
GuestThreadBlocking.NoteUnblocked(currentThreadId);
|
||||
}
|
||||
|
||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
@@ -1260,22 +1262,30 @@ public static class KernelPthreadCompatExports
|
||||
var deadline = timed
|
||||
? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec))
|
||||
: long.MaxValue;
|
||||
while (state.SignalsPending == 0 && !GuestThreadBlocking.ShutdownRequested)
|
||||
GuestThreadBlocking.NoteBlocked(currentThreadId, timed ? "pthread_cond_timedwait" : "pthread_cond_wait");
|
||||
try
|
||||
{
|
||||
var remaining = timed
|
||||
? GetRemainingTimeout(deadline)
|
||||
: TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
if (timed && remaining <= TimeSpan.Zero)
|
||||
while (state.SignalsPending == 0 && !GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var remaining = timed
|
||||
? GetRemainingTimeout(deadline)
|
||||
: TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
if (timed && remaining <= TimeSpan.Zero)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (remaining > TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds))
|
||||
{
|
||||
remaining = TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
}
|
||||
if (remaining > TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds))
|
||||
{
|
||||
remaining = TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
}
|
||||
|
||||
_ = Monitor.Wait(state.SyncRoot, remaining);
|
||||
_ = Monitor.Wait(state.SyncRoot, remaining);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadBlocking.NoteUnblocked(currentThreadId);
|
||||
}
|
||||
|
||||
if (state.SignalsPending > 0)
|
||||
|
||||
@@ -91,10 +91,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
|
||||
}
|
||||
|
||||
// On the outer class deliberately: a static on the nested state class gives it a type
|
||||
// initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||
private static long _nextRwlockWakeId;
|
||||
|
||||
private sealed class PthreadRwlockState
|
||||
{
|
||||
public object SyncRoot { get; } = new();
|
||||
@@ -105,8 +101,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
public ulong WriterThreadId { get; set; }
|
||||
public int WaitingWriters { get; set; }
|
||||
|
||||
// See PthreadMutexState.WakeKey.
|
||||
public string WakeKey { get; } = "pthread_rwlock#" + Interlocked.Increment(ref _nextRwlockWakeId).ToString("X");
|
||||
|
||||
public int GetReaderCount(ulong threadId)
|
||||
{
|
||||
@@ -168,17 +162,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RwlockWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required PthreadRwlockState Rwlock { get; init; }
|
||||
public required ulong ThreadId { get; init; }
|
||||
public required bool Write { get; init; }
|
||||
|
||||
public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
||||
public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write);
|
||||
}
|
||||
|
||||
private readonly record struct TlsKeyState(ulong Destructor);
|
||||
|
||||
private readonly record struct PthreadAttrState(
|
||||
@@ -1184,7 +1167,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1479,35 +1461,29 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// In-place block: Monitor.Wait releases SyncRoot and parks
|
||||
// atomically, so an unlock's PulseAll cannot be lost. Sliced
|
||||
// only so teardown can unwind parked threads.
|
||||
rwlock.WaitingWriters++;
|
||||
var transferredToScheduler = false;
|
||||
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_rwlock_wrlock");
|
||||
try
|
||||
{
|
||||
if (GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_rwlock_wrlock",
|
||||
rwlock.WakeKey,
|
||||
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true }))
|
||||
{
|
||||
transferredToScheduler = true;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||
{
|
||||
Monitor.Wait(rwlock.SyncRoot);
|
||||
if (GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||
}
|
||||
|
||||
_ = Monitor.Wait(rwlock.SyncRoot, GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
}
|
||||
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!transferredToScheduler)
|
||||
{
|
||||
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||
}
|
||||
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||
GuestThreadBlocking.NoteUnblocked(currentThreadId);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1517,20 +1493,25 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
}
|
||||
|
||||
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
{
|
||||
if (GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_rwlock_rdlock",
|
||||
rwlock.WakeKey,
|
||||
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
|
||||
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_rwlock_rdlock");
|
||||
try
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
{
|
||||
if (GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||
}
|
||||
|
||||
Monitor.Wait(rwlock.SyncRoot);
|
||||
_ = Monitor.Wait(rwlock.SyncRoot, GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadBlocking.NoteUnblocked(currentThreadId);
|
||||
}
|
||||
}
|
||||
|
||||
if (rwlock.WriterThreadId != 0 ||
|
||||
@@ -1547,33 +1528,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryAcquireBlockedRwlock(PthreadRwlockState rwlock, ulong currentThreadId, bool write)
|
||||
{
|
||||
lock (rwlock.SyncRoot)
|
||||
{
|
||||
if (write)
|
||||
{
|
||||
if (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DetectRwlockWriterConflict(0, rwlock, currentThreadId, "wrlock-resume");
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
rwlock.AddReader(currentThreadId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Call while holding lock(rwlock.SyncRoot): an existing reader/writer here means a
|
||||
// writer would share the rwlock with another holder — a data race.
|
||||
private static void DetectRwlockWriterConflict(ulong resolvedAddress, PthreadRwlockState rwlock, ulong currentThreadId, string site)
|
||||
@@ -1604,8 +1558,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
rwlock.GetReaderCount(currentThreadId) == 0;
|
||||
}
|
||||
|
||||
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
|
||||
|
||||
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
|
||||
{
|
||||
PthreadRwlockState? rwlock;
|
||||
|
||||
@@ -127,6 +127,7 @@ public static class KernelSemaphoreCompatExports
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
|
||||
}
|
||||
|
||||
GuestThreadBlocking.NoteBlocked(GuestThreadExecution.CurrentGuestThreadHandle, "sceKernelWaitSema");
|
||||
try
|
||||
{
|
||||
while (semaphore.Count < needCount)
|
||||
@@ -159,6 +160,7 @@ public static class KernelSemaphoreCompatExports
|
||||
finally
|
||||
{
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
GuestThreadBlocking.NoteUnblocked(GuestThreadExecution.CurrentGuestThreadHandle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user