241 lines
12 KiB
C#
241 lines
12 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.Core.Cpu.Native;
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.Libs.Diagnostics;
|
|
using SharpEmu.Libs.VideoOut;
|
|
using System.Reflection;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Cpu;
|
|
|
|
[CollectionDefinition("GuestThreadFlowProfile", DisableParallelization = true)]
|
|
public sealed class GuestThreadFlowProfileCollection;
|
|
|
|
[Collection("GuestThreadFlowProfile")]
|
|
public sealed class GuestThreadFlowProfileTests
|
|
{
|
|
[NativeX64Fact]
|
|
public void NewSessionReopensAllClosedTraces()
|
|
{
|
|
var modules = new ModuleManager();
|
|
modules.Freeze();
|
|
using var backend = new DirectExecutionBackend(modules);
|
|
var backendType = typeof(DirectExecutionBackend);
|
|
const BindingFlags instancePrivate = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
var profile = Assert.IsType<GuestThreadFlowProfile>(backendType
|
|
.GetField("_guestThreadFlowProfile", instancePrivate)!.GetValue(backend));
|
|
profile.Record(CreateEvent(1));
|
|
profile.Close();
|
|
SemaphoreSignalProfile.Close();
|
|
MutexHandoffProfile.Close();
|
|
SubmissionFlowProfile.WriteTrace(TextWriter.Null);
|
|
backendType.GetMethod("StartGuestFlowTraces", instancePrivate)!.Invoke(backend, null);
|
|
profile.Record(CreateEvent(2));
|
|
Assert.Equal(CreateEvent(2), Assert.Single(profile.Close().Events));
|
|
SemaphoreSignalProfile.Begin(0x83, 1);
|
|
MutexHandoffProfile.Record(1, "Granted", 0x100, 0x200);
|
|
Assert.Equal(RenderPhaseProfile.FrameTraceEnabled ? 1 : 0, SemaphoreSignalProfile.Close().Events.Length);
|
|
Assert.Equal(RenderPhaseProfile.FrameTraceEnabled ? 1 : 0, MutexHandoffProfile.Close().Events.Length);
|
|
SubmissionFlowProfile.Record(SubmissionFlowProfile.EventKind.SubmitEntered);
|
|
using var submissionOutput = new StringWriter();
|
|
SubmissionFlowProfile.WriteTrace(submissionOutput);
|
|
Assert.Equal(RenderPhaseProfile.FrameTraceEnabled, submissionOutput.ToString().Contains("[PERF][SUBMISSION_FLOW_TRACE]"));
|
|
}
|
|
|
|
[NativeX64Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void TraceOutputFailureDoesNotInterruptBackendCleanup(bool disposedOutput)
|
|
{
|
|
var modules = new ModuleManager();
|
|
modules.Freeze();
|
|
using var backend = new DirectExecutionBackend(modules);
|
|
var backendType = typeof(DirectExecutionBackend);
|
|
const BindingFlags instancePrivate = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
var profile = Assert.IsType<GuestThreadFlowProfile>(backendType
|
|
.GetField("_guestThreadFlowProfile", instancePrivate)!.GetValue(backend));
|
|
profile.Record(CreateEvent(1));
|
|
using var output = new FailingTraceWriter(disposedOutput);
|
|
backendType.GetMethod("WriteGuestFlowTraces", instancePrivate)!.Invoke(backend, [output]);
|
|
Assert.Empty(profile.Close().Events);
|
|
}
|
|
|
|
private sealed class FailingTraceWriter(bool disposedOutput) : StringWriter
|
|
{
|
|
public override void WriteLine(string? value)
|
|
{
|
|
if (disposedOutput) throw new ObjectDisposedException(nameof(FailingTraceWriter));
|
|
throw new IOException("The trace output is unavailable.");
|
|
}
|
|
}
|
|
|
|
[NativeX64Fact]
|
|
public void DetailedTraceDoesNotLetReadyThreadBypassActiveExecutor()
|
|
{
|
|
var modules = new ModuleManager();
|
|
modules.Freeze();
|
|
using var backend = new DirectExecutionBackend(modules);
|
|
var backendType = typeof(DirectExecutionBackend);
|
|
const BindingFlags instancePrivate = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
var threadType = backendType.GetNestedType("GuestThreadState", BindingFlags.NonPublic)!;
|
|
var runStateType = backendType.GetNestedType("GuestThreadRunState", BindingFlags.NonPublic)!;
|
|
var thread = Activator.CreateInstance(threadType)!;
|
|
threadType.GetProperty("ThreadHandle")!.SetValue(thread, 0x1234UL);
|
|
threadType.GetProperty("State")!.SetValue(thread, Enum.Parse(runStateType, "Ready"));
|
|
threadType.GetProperty("ExecutorActive")!.SetValue(thread, true);
|
|
var queue = backendType.GetField("_readyGuestThreads", instancePrivate)!.GetValue(backend)!;
|
|
queue.GetType().GetMethod("Enqueue")!.Invoke(queue, [thread]);
|
|
backendType.GetField("_readyGuestThreadCount", instancePrivate)!.SetValue(backend, 1);
|
|
var claim = backendType.GetMethod("TryClaimReadyGuestThreadLocked", instancePrivate)!;
|
|
object?[] claimArguments = [null];
|
|
Assert.False((bool)claim.Invoke(backend, claimArguments)!);
|
|
Assert.Null(claimArguments[0]);
|
|
Assert.Equal(1, backendType.GetField("_readyGuestThreadCount", instancePrivate)!.GetValue(backend));
|
|
Assert.Equal("Ready", threadType.GetProperty("State")!.GetValue(thread)!.ToString());
|
|
object?[] releaseArguments = [thread, null];
|
|
lock (backendType.GetField("_guestThreadGate", instancePrivate)!.GetValue(backend)!)
|
|
Assert.False((bool)backendType.GetMethod("TryReleaseGuestThreadExecutorLocked", instancePrivate)!
|
|
.Invoke(backend, releaseArguments)!);
|
|
Assert.True((bool)claim.Invoke(backend, claimArguments)!);
|
|
Assert.Same(thread, claimArguments[0]);
|
|
Assert.True((bool)threadType.GetProperty("ExecutorActive")!.GetValue(thread)!);
|
|
var profile = (GuestThreadFlowProfile)backendType.GetField("_guestThreadFlowProfile", instancePrivate)!.GetValue(backend)!;
|
|
var snapshot = profile.Close();
|
|
if (GuestThreadFlowProfile.Enabled)
|
|
Assert.Equal(new[] { GuestThreadFlowProfile.EventKind.ClaimDeferred,
|
|
GuestThreadFlowProfile.EventKind.ExecutorReleased, GuestThreadFlowProfile.EventKind.Claimed },
|
|
snapshot.Events.Select(item => item.Kind));
|
|
else
|
|
Assert.Empty(snapshot.Events);
|
|
}
|
|
|
|
[NativeX64Fact]
|
|
public void ShutdownRequestWritesTraceBeforeBackendDisposal()
|
|
{
|
|
var modules = new ModuleManager();
|
|
modules.Freeze();
|
|
using var backend = new DirectExecutionBackend(modules);
|
|
var profile = Assert.IsType<GuestThreadFlowProfile>(typeof(DirectExecutionBackend)
|
|
.GetField("_guestThreadFlowProfile", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(backend));
|
|
profile.Record(CreateEvent(1));
|
|
var previousOutput = Console.Error;
|
|
using var output = new StringWriter();
|
|
try
|
|
{
|
|
SemaphoreSignalProfile.StartSession();
|
|
SemaphoreSignalProfile.Begin(0x83, 1);
|
|
MutexHandoffProfile.StartSession();
|
|
MutexHandoffProfile.Record(1, "Granted", 0x100, 0x200);
|
|
SubmissionFlowProfile.StartSession();
|
|
SubmissionFlowProfile.Record(SubmissionFlowProfile.EventKind.SubmitEntered);
|
|
Console.SetError(output);
|
|
backend.RequestHostShutdown("trace-test");
|
|
Assert.Contains("[PERF][GUEST_FLOW_TRACE]", output.ToString());
|
|
Assert.Contains("[PERF][GUEST_FLOW]", output.ToString());
|
|
if (RenderPhaseProfile.FrameTraceEnabled)
|
|
Assert.Contains("[PERF][SEMAPHORE_SIGNAL_TRACE]", output.ToString());
|
|
backend.RequestHostShutdown("trace-test");
|
|
Assert.Equal(1, output.ToString().Split("[PERF][GUEST_FLOW_TRACE]").Length - 1);
|
|
Assert.Equal(RenderPhaseProfile.FrameTraceEnabled ? 1 : 0,
|
|
output.ToString().Split("[PERF][SEMAPHORE_SIGNAL_TRACE]").Length - 1);
|
|
Assert.Equal(RenderPhaseProfile.FrameTraceEnabled ? 1 : 0,
|
|
output.ToString().Split("[PERF][MUTEX_HANDOFF_TRACE]").Length - 1);
|
|
Assert.Equal(RenderPhaseProfile.FrameTraceEnabled ? 1 : 0,
|
|
output.ToString().Split("[PERF][SUBMISSION_FLOW_TRACE]").Length - 1);
|
|
Assert.Empty(MutexHandoffProfile.Close().Events);
|
|
Assert.Empty(SemaphoreSignalProfile.Close().Events);
|
|
Assert.Empty(profile.Close().Events);
|
|
}
|
|
finally
|
|
{
|
|
Console.SetError(previousOutput);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TraceFlushesTheWriterBeforeReturning()
|
|
{
|
|
var profile = new GuestThreadFlowProfile(1);
|
|
profile.Record(CreateEvent(1));
|
|
using var output = new FlushTrackingWriter();
|
|
profile.WriteTrace(output);
|
|
Assert.True(output.Flushed);
|
|
Assert.Contains("[PERF][GUEST_FLOW]", output.ToString());
|
|
}
|
|
|
|
private sealed class FlushTrackingWriter : StringWriter
|
|
{
|
|
internal bool Flushed { get; private set; }
|
|
public override void Flush() => Flushed = true;
|
|
}
|
|
|
|
[Fact]
|
|
public void BufferRetainsNewestCompleteEventsAndCountsOverwrites()
|
|
{
|
|
var profile = new GuestThreadFlowProfile(3);
|
|
for (var index = 0; index < 8; index++) profile.Record(CreateEvent(index));
|
|
var snapshot = profile.Close();
|
|
Assert.Equal(8, snapshot.TotalEvents);
|
|
Assert.Equal(new[] { CreateEvent(5), CreateEvent(6), CreateEvent(7) }, snapshot.Events);
|
|
profile.Record(CreateEvent(9));
|
|
Assert.Empty(profile.Close().Events);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConcurrentWritersDoNotMixEventFields()
|
|
{
|
|
var profile = new GuestThreadFlowProfile(128);
|
|
Parallel.For(0, 10000, index => profile.Record(CreateEvent(index)));
|
|
var snapshot = profile.Close();
|
|
Assert.Equal(10000, snapshot.TotalEvents);
|
|
Assert.Equal(128, snapshot.Events.Length);
|
|
Assert.Equal(128, snapshot.Events.Select(item => item.Timestamp).Distinct().Count());
|
|
Assert.All(snapshot.Events, item => Assert.Equal(CreateEvent((int)item.Timestamp), item));
|
|
}
|
|
|
|
[Fact]
|
|
public void TraceKeepsGuestIdentityWaitKeyAndArgumentsOnOneLine()
|
|
{
|
|
var profile = new GuestThreadFlowProfile(3);
|
|
profile.Record(CreateEvent(1) with { Name = "worker'\r\nname", Kind = GuestThreadFlowProfile.EventKind.Blocked });
|
|
profile.Record(CreateEvent(2) with { HostThreadId = 90, GuestThreadHandle = 4, Kind = GuestThreadFlowProfile.EventKind.Ready });
|
|
using var output = new StringWriter();
|
|
profile.WriteTrace(output);
|
|
var text = output.ToString();
|
|
Assert.Equal(3, text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Length);
|
|
Assert.Contains("guest=0x4", text);
|
|
Assert.Contains("name='worker\\'\\r\\nname'", text);
|
|
Assert.Contains("wake='wait:1'", text);
|
|
Assert.Contains("arg0=0x5 arg1=0x6 deadline=7", text);
|
|
Assert.Contains("thread=90 guest=0x4", text);
|
|
profile.WriteTrace(output);
|
|
Assert.Equal(text, output.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void ConcurrentCloseCapturesOnlyCompleteRecords()
|
|
{
|
|
var profile = new GuestThreadFlowProfile(10000);
|
|
GuestThreadFlowProfile.Snapshot snapshot = default;
|
|
Parallel.Invoke(() => Parallel.For(0, 10000, index => profile.Record(CreateEvent(index))),
|
|
() => snapshot = profile.Close());
|
|
Assert.Equal(snapshot.TotalEvents, snapshot.Events.Length);
|
|
Assert.All(snapshot.Events, item => Assert.Equal(CreateEvent((int)item.Timestamp), item));
|
|
Assert.Empty(profile.Close().Events);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(-1)]
|
|
public void BufferRequiresPositiveCapacity(int capacity)
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new GuestThreadFlowProfile(capacity));
|
|
}
|
|
|
|
private static GuestThreadFlowProfile.TraceEvent CreateEvent(int index) => new(index, index + 2,
|
|
(ulong)index + 3, "worker", GuestThreadFlowProfile.EventKind.Blocked, "wait", $"wait:{index}",
|
|
"import", (ulong)index + 4, (ulong)index + 5, index + 6);
|
|
}
|