Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions DS4Windows/DS4Control/DTOXml/ProfileDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3976,6 +3976,17 @@ public void PostProcessXml()
//OutputContDevice +Done
}

// The framework only caches XmlSerializer(Type) and XmlSerializer(Type, string).
// Constructing one with XmlAttributeOverrides generates and loads a new dynamic
// assembly every time (~100 ms with first use) and that assembly never unloads.
// Serialize/Deserialize are thread-safe, so one shared instance is used instead.
private static readonly Lazy<XmlSerializer> serializer =
new Lazy<XmlSerializer>(
() => new XmlSerializer(typeof(ProfileDTO), GetAttributeOverrides()),
System.Threading.LazyThreadSafetyMode.ExecutionAndPublication);

internal static XmlSerializer Serializer => serializer.Value;

public static XmlAttributeOverrides GetAttributeOverrides()
{
XmlAttributeOverrides xmlOverrides = new XmlAttributeOverrides();
Expand Down
3 changes: 1 addition & 2 deletions DS4Windows/DS4Control/PreparedProfileLoad.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ internal static bool TryPrepare(string path, int device,
}
}

var serializer = new XmlSerializer(typeof(ProfileDTO),
ProfileDTO.GetAttributeOverrides());
var serializer = ProfileDTO.Serializer;
using var reader = new StringReader(xml);
var candidate = serializer.Deserialize(reader) as ProfileDTO;
if (candidate == null)
Expand Down
3 changes: 1 addition & 2 deletions DS4Windows/DS4Control/ScpUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5574,8 +5574,7 @@ public bool SaveProfileNew(int device, string proName)
string path = Path.Combine(Global.appdatapath, "Profiles",
$"{proName}{Global.XML_EXTENSION}");
string testStr = string.Empty;
XmlSerializer serializer = new XmlSerializer(typeof(ProfileDTO),
ProfileDTO.GetAttributeOverrides());
XmlSerializer serializer = ProfileDTO.Serializer;
using (Utf8StringWriter strWriter = new Utf8StringWriter())
{
using XmlWriter xmlWriter = XmlWriter.Create(strWriter,
Expand Down
83 changes: 83 additions & 0 deletions DS4WindowsTests/ProfileSerializerCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using DS4Windows;
using DS4Windows.DS4Control;
using DS4WinWPF.DS4Control.DTOXml;

namespace DS4WindowsTests;

/// <summary>
/// The ProfileDTO serializer uses XmlAttributeOverrides, which .NET does not
/// cache. These tests check it is built once and that profile preparation does
/// not load a new assembly per call.
/// </summary>
[TestClass]
[DoNotParallelize]
public class ProfileSerializerCacheTests
{
private string folder;

[TestInitialize]
public void Setup()
{
folder = Path.Combine(Path.GetTempPath(), $"ds4w-serializer-cache-{Guid.NewGuid():N}");
Directory.CreateDirectory(folder);
}

[TestCleanup]
public void Cleanup()
{
try { Directory.Delete(folder, true); } catch (IOException) { }
}

[TestMethod]
public void Serializer_ReturnsSameInstance()
{
Assert.IsNotNull(ProfileDTO.Serializer);
Assert.AreSame(ProfileDTO.Serializer, ProfileDTO.Serializer);
}

[TestMethod]
public void RepeatedTryPrepare_DoesNotGrowAssemblyCount()
{
string path = Path.Combine(folder, "Aim.xml");
File.WriteAllText(path, CreateSavedProfileXml(
"custom", "0.72, 0.26, 1.00, 1.00"), Encoding.UTF8);

void Prepare()
{
bool ok = PreparedProfileLoad.TryPrepare(path, 0, out _, out _, out string error);
Assert.IsTrue(ok, error);
}

for (int i = 0; i < 3; i++)
Prepare();
int before = AppDomain.CurrentDomain.GetAssemblies().Length;
for (int i = 0; i < 20; i++)
Prepare();
int after = AppDomain.CurrentDomain.GetAssemblies().Length;

Assert.AreEqual(before, after, "TryPrepare loaded new assemblies per call.");
}

// Builds a profile the way BackingStore.SaveProfileNew writes one.
private static string CreateSavedProfileXml(string rsCurveMode, string rsCurveCustom)
{
var serializer = new XmlSerializer(typeof(ProfileDTO), ProfileDTO.GetAttributeOverrides());
using var strWriter = new Utf8StringWriter();
using (XmlWriter xmlWriter = XmlWriter.Create(strWriter,
new XmlWriterSettings { Encoding = Encoding.UTF8, Indent = true }))
{
xmlWriter.WriteComment(" DS4Windows Configuration Data. test ");
xmlWriter.WriteWhitespace("\r\n\r\n");
var dto = new ProfileDTO { DeviceIndex = 0 };
dto.MapFrom(new BackingStore());
dto.RSOutputCurveMode = rsCurveMode;
dto.RSOutputCurveCustom = rsCurveCustom;
serializer.Serialize(xmlWriter, dto,
new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }));
}
return strWriter.ToString();
}
}