sharpemu/tests/SharpEmu.Libs.Tests/Memory/GuestMemory/GuestMemoryProfileTests.cs

163 lines
6.8 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.GuestMemory;
using SharpEmu.Libs.Tests.Memory.GpuMemory;
using Xunit;
namespace SharpEmu.Libs.Tests.Memory.GuestMemory;
[Collection(GpuMemoryStateCollection.Name)]
public sealed class GuestMemoryProfileTests
{
[Fact]
public void ReadbackDetailsSeparateCallersForTheSameRange()
{
var measurements = new GuestMemoryProfile.ReadbackMeasurements();
var commandRange = new GuestMemoryProfile.ReadbackRange(0x200000, 0x4000, false,
GuestMemoryProfile.ReadbackSource.CommandMemoryRead);
var shaderRange = commandRange with { Source = GuestMemoryProfile.ReadbackSource.ShaderResourceRead };
measurements.Record(commandRange, 608, 11);
measurements.Record(shaderRange, 780, 7);
measurements.Record(commandRange, 608, 3);
var snapshot = measurements.TakeSnapshot();
Assert.Equal(2, snapshot.Ranges.Length);
Assert.Equal(new GuestMemoryProfile.ReadbackMeasurement(2, 2, 1216, 14, 11),
snapshot.Ranges.Single(entry => entry.Key == commandRange).Value);
Assert.Equal(new GuestMemoryProfile.ReadbackMeasurement(1, 1, 780, 7, 7),
snapshot.Ranges.Single(entry => entry.Key == shaderRange).Value);
Assert.Empty(measurements.TakeSnapshot().Ranges);
}
[Fact]
public void ReadbackReportRequiresBothSwitchesAndIncludesTheCaller()
{
GuestMemoryProfile.WriteReport();
using var output = new StringWriter();
var previous = Console.Error;
try
{
Console.SetError(output);
GuestMemoryProfile.RecordBufferReadback(0x200000, 0x4000, false, 608, 1,
GuestMemoryProfile.ReadbackSource.ShaderResourceRead);
GuestMemoryProfile.WriteReport();
}
finally
{
Console.SetError(previous);
}
var enabled = Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE") == "1" &&
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE") == "1";
if (enabled)
Assert.Contains("source=ShaderResourceRead calls=1 downloads=1 bytes=608", output.ToString());
else
Assert.DoesNotContain("[BUFFER_READBACK]", output.ToString());
}
[Fact]
public void ReadbackDetailsSeparateDownloadsFromCleanRequests()
{
var measurements = new GuestMemoryProfile.ReadbackMeasurements();
var range = new GuestMemoryProfile.ReadbackRange(0x200000, 0x80000, false);
measurements.Record(range, 4096, 11);
measurements.Record(range, 0, 3);
measurements.Record(range with { CpuWrite = true }, 8192, 7);
var snapshot = measurements.TakeSnapshot();
Assert.Equal(new GuestMemoryProfile.ReadbackMeasurement(2, 1, 4096, 14, 11),
snapshot.Ranges.Single(entry => entry.Key == range).Value);
Assert.Equal(2, snapshot.Ranges.Length);
Assert.Equal(0, snapshot.UntrackedCalls);
Assert.Empty(measurements.TakeSnapshot().Ranges);
}
[Fact]
public void ReadbackDetailsBoundDistinctRangesAndReportOverflow()
{
var measurements = new GuestMemoryProfile.ReadbackMeasurements();
for (var index = 0; index <= GuestMemoryProfile.ReadbackMeasurements.Capacity; index++)
measurements.Record(new GuestMemoryProfile.ReadbackRange((ulong)index * 0x80000, 0x80000, false), 1, 1);
measurements.Record(new GuestMemoryProfile.ReadbackRange(0, 0x80000, false), 1, 1);
var snapshot = measurements.TakeSnapshot();
Assert.Equal(GuestMemoryProfile.ReadbackMeasurements.Capacity, snapshot.Ranges.Length);
Assert.Equal(1, snapshot.UntrackedCalls);
Assert.Equal(2, snapshot.Ranges.Single(entry => entry.Key.Address == 0).Value.Calls);
Assert.Equal(0, measurements.TakeSnapshot().UntrackedCalls);
}
[Fact]
public void ReportUsesThePerformanceProfileSwitch()
{
GuestMemoryProfile.WriteReport();
using var output = new StringWriter();
var previous = Console.Error;
try
{
Console.SetError(output);
using (GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.ReservationHost)) { }
GuestMemoryProfile.WriteReport();
}
finally
{
Console.SetError(previous);
}
var enabled = Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE") == "1";
if (enabled)
Assert.Contains("operation=ReservationHost calls=1 inclusive_ms=", output.ToString());
else
Assert.Equal(string.Empty, output.ToString());
}
[Fact]
public void SnapshotReportsCountTotalAndMaximumThenClearsTheWindow()
{
var measurements = new GuestMemoryProfile.Measurements();
measurements.Record(GuestMemoryProfile.Operation.MappingDrain, 7);
measurements.Record(GuestMemoryProfile.Operation.MappingDrain, 11);
var snapshot = measurements.TakeSnapshot();
Assert.Equal(new GuestMemoryProfile.Measurement(2, 18, 11),
snapshot[(int)GuestMemoryProfile.Operation.MappingDrain]);
Assert.All(measurements.TakeSnapshot(), value => Assert.Equal(default, value));
Assert.Equal(2, snapshot[(int)GuestMemoryProfile.Operation.MappingDrain].Calls);
}
[Fact]
public void NestedOperationTotalsRemainSeparate()
{
var measurements = new GuestMemoryProfile.Measurements();
measurements.Record(GuestMemoryProfile.Operation.ReservationSearch, 4);
measurements.Record(GuestMemoryProfile.Operation.ReservationHost, 9);
measurements.Record(GuestMemoryProfile.Operation.Reservation, 20);
var snapshot = measurements.TakeSnapshot();
Assert.Equal(4, snapshot[(int)GuestMemoryProfile.Operation.ReservationSearch].Ticks);
Assert.Equal(9, snapshot[(int)GuestMemoryProfile.Operation.ReservationHost].Ticks);
Assert.Equal(20, snapshot[(int)GuestMemoryProfile.Operation.Reservation].Ticks);
}
[Fact]
public void ConcurrentRecordsAndReportsDoNotLoseCalls()
{
var measurements = new GuestMemoryProfile.Measurements();
long calls = 0;
long ticks = 0;
Parallel.For(0, 1000, iteration =>
{
measurements.Record(GuestMemoryProfile.Operation.MappingApply, 3);
if (iteration % 10 != 0)
return;
var value = measurements.TakeSnapshot()[(int)GuestMemoryProfile.Operation.MappingApply];
Interlocked.Add(ref calls, value.Calls);
Interlocked.Add(ref ticks, value.Ticks);
});
var remaining = measurements.TakeSnapshot()[(int)GuestMemoryProfile.Operation.MappingApply];
Assert.Equal(1000, calls + remaining.Calls);
Assert.Equal(3000, ticks + remaining.Ticks);
}
}