sharpemu/tests/SharpEmu.Libs.Tests/Kernel/MutexHandoffProfileTests.cs

157 lines
6.8 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
using SharpEmu.HLE;
using SharpEmu.Libs.Diagnostics;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.Tests.Pthread;
using Xunit;
namespace SharpEmu.Libs.Tests.Kernel;
[Collection("GuestThreadFlowProfile")]
public sealed class MutexHandoffProfileTests
{
[Fact]
public void BufferRetainsWholeRecordsAndClosesUntilReset()
{
var buffer = new MutexHandoffProfile.EventBuffer(2);
for (var index = 0; index < 4; index++) buffer.Record(CreateEvent(index));
var snapshot = buffer.Close();
Assert.Equal(4, snapshot.TotalEvents);
Assert.Equal(new[] { CreateEvent(2), CreateEvent(3) }, snapshot.Events);
buffer.Record(CreateEvent(4));
Assert.Empty(buffer.Close().Events);
buffer.Reset();
buffer.Record(CreateEvent(5));
Assert.Equal(CreateEvent(5), Assert.Single(buffer.Close().Events));
}
[Fact]
public void ConcurrentWritesKeepIdentityAndOwnershipTogether()
{
var buffer = new MutexHandoffProfile.EventBuffer(128);
Parallel.For(0, 10000, index => buffer.Record(CreateEvent(index)));
var snapshot = buffer.Close();
Assert.Equal(10000, snapshot.TotalEvents);
Assert.Equal(128, snapshot.Events.Length);
Assert.All(snapshot.Events, item => Assert.Equal(CreateEvent((int)item.Timestamp), item));
}
[Fact]
public void MutexHandoffKeepsFifoOwnershipAndWakeArguments()
{
var exports = typeof(KernelPthreadCompatExports);
var stateType = exports.GetNestedType("PthreadMutexState", BindingFlags.NonPublic)!;
var state = Activator.CreateInstance(stateType, nonPublic: true)!;
var owner = stateType.GetProperty("OwnerThreadId")!;
var enqueue = exports.GetMethod("EnqueueMutexWaiterLocked", BindingFlags.Static | BindingFlags.NonPublic)!;
var grant = exports.GetMethod("TryGrantMutexWaiterLocked", BindingFlags.Static | BindingFlags.NonPublic)!;
var wake = exports.GetMethod("WakeProfiledMutexWaiter", BindingFlags.Static | BindingFlags.NonPublic)!;
var scheduler = DispatchProxy.Create<IGuestThreadScheduler, MutexWakeScheduler>();
var previousScheduler = GuestThreadExecution.Scheduler;
var previousGuest = GuestThreadExecution.EnterGuestThread(0x10);
MutexHandoffProfile.StartSession();
try
{
GuestThreadExecution.Scheduler = scheduler;
owner.SetValue(state, 0x10UL);
var first = enqueue.Invoke(null, [state, 0x20UL, true, "mutex-test:first"])!;
var second = enqueue.Invoke(null, [state, 0x30UL, true, "mutex-test:second"])!;
Assert.False((bool)grant.Invoke(null, [state, first])!);
owner.SetValue(state, 0UL);
Assert.False((bool)grant.Invoke(null, [state, second])!);
Assert.True((bool)grant.Invoke(null, [state, first])!);
Assert.Equal(0x20UL, owner.GetValue(state));
wake.Invoke(null, [state, first]);
Assert.Equal(1, ((MutexWakeScheduler)(object)scheduler).Calls);
Assert.Equal(0x20UL, owner.GetValue(state));
var snapshot = MutexHandoffProfile.Close();
if (!MutexHandoffProfile.Enabled)
{
Assert.Empty(snapshot.Events);
return;
}
Assert.Equal(new[] { "Queued", "Queued", "Granted", "WakeStarted", "WakeFinished" },
snapshot.Events.Select(item => item.Stage));
Assert.Equal(0x10UL, snapshot.Events[0].Owner);
Assert.Equal(0x20UL, snapshot.Events[2].Owner);
Assert.Equal(0x20UL, snapshot.Events[2].Waiter);
Assert.Equal("mutex-test:first", snapshot.Events[2].WakeKey);
Assert.Equal(1, snapshot.Events[2].Waiting);
Assert.Equal(1, snapshot.Events[^1].Result);
Assert.All(snapshot.Events, item =>
{
Assert.Equal(snapshot.Events[0].MutexIdentity, item.MutexIdentity);
Assert.Equal(0x10UL, item.GuestThread);
});
}
finally
{
GuestThreadExecution.Scheduler = previousScheduler;
GuestThreadExecution.RestoreGuestThread(previousGuest);
MutexHandoffProfile.Close();
}
}
[Fact]
public void PublicMutexOperationsRecordAddressOnlyWhenDetailedTraceIsEnabled()
{
const ulong memoryBase = 0x6A0000000;
const ulong address = memoryBase + 0x100;
var context = new CpuContext(new PthreadMutexSemanticsTests.AllocatingCpuMemory(memoryBase, 0x4000), Generation.Gen5);
context[CpuRegister.Rdi] = address;
Assert.True(context.TryWriteUInt64(address, 1));
MutexHandoffProfile.StartSession();
try
{
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context));
var snapshot = MutexHandoffProfile.Close();
if (!MutexHandoffProfile.Enabled)
{
Assert.Empty(snapshot.Events);
Assert.Equal(0, MutexHandoffProfile.CreateIdentity());
return;
}
Assert.Equal(new[] { "lock", "Released", "unlock" }, snapshot.Events.Select(item => item.Stage));
Assert.All(snapshot.Events, item => Assert.Equal(address, item.Address));
Assert.NotEqual(0UL, snapshot.Events[0].Owner);
Assert.Equal(0UL, snapshot.Events[1].Owner);
Assert.True(snapshot.Events[0].MutexIdentity > 0);
}
finally
{
KernelPthreadCompatExports.PthreadMutexDestroy(context);
MutexHandoffProfile.Close();
}
}
[Fact]
public void OutputIncludesOwnerWaiterAndWakeKey()
{
using var output = new StringWriter();
MutexHandoffProfile.WriteTrace(output, new MutexHandoffProfile.Snapshot([CreateEvent(1)], 4));
Assert.Contains("retained=1 overwritten=3", output.ToString());
Assert.Contains("mutex=4 stage=Granted address=0x5 owner=0x6 waiter=0x7 wake=mutex:1 waiting=8 result=9", output.ToString());
}
public class MutexWakeScheduler : DispatchProxy
{
public int Calls { get; private set; }
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
Assert.Equal(nameof(IGuestThreadScheduler.WakeBlockedThreads), targetMethod!.Name);
Assert.Equal("mutex-test:first", args![0]);
Assert.Equal(1, args[1]);
Calls++;
return 1;
}
}
private static MutexHandoffProfile.TraceEvent CreateEvent(int index) => new(index, index + 1,
(ulong)index + 2, index + 3, "Granted", (ulong)index + 4, (ulong)index + 5,
(ulong)index + 6, $"mutex:{index}", index + 7, index + 8);
}