diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj index fbf324e611..5a65868663 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj +++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj @@ -27,6 +27,7 @@ + diff --git a/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs b/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs index cf26d90e6b..8abbf910d7 100644 --- a/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs +++ b/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs @@ -1,6 +1,6 @@ -using System; +using BenchmarkDotNet.Serialization; using System.Linq; -using SimpleJson; +using System.Text.Json.Serialization; #nullable enable @@ -8,14 +8,31 @@ namespace BenchmarkDotNet.Disassemblers { internal struct ClrMdArgs(int processId, string typeName, string methodName, bool printSource, int maxDepth, string syntax, string tfm, string[] filters, string resultsPath = "") { + [JsonIgnore] internal int ProcessId = processId; - internal string TypeName = typeName; + + [JsonIgnore] + internal string TypeName = typeName ?? ""; + + [JsonInclude] internal string MethodName = methodName; + + [JsonInclude] internal bool PrintSource = printSource; + + [JsonInclude] internal int MaxDepth = methodName == DisassemblerConstants.DisassemblerEntryMethodName && maxDepth != int.MaxValue ? maxDepth + 1 : maxDepth; + + [JsonInclude] internal string[] Filters = filters; + + [JsonInclude] internal string Syntax = syntax; + + [JsonInclude] internal string TargetFrameworkMoniker = tfm; + + [JsonInclude] internal string ResultsPath = resultsPath; internal static ClrMdArgs FromArgs(string[] args) @@ -30,46 +47,5 @@ internal static ClrMdArgs FromArgs(string[] args) tfm: args[7], filters: [.. args.Skip(8)] ); - - internal readonly string Serialize() - { - SimpleJsonSerializer.CurrentJsonSerializerStrategy.Indent = false; - var jsonObject = new JsonObject() - { - [nameof(MethodName)] = MethodName, - [nameof(PrintSource)] = PrintSource, - [nameof(MaxDepth)] = MaxDepth, - [nameof(Syntax)] = Syntax, - [nameof(TargetFrameworkMoniker)] = TargetFrameworkMoniker, - [nameof(ResultsPath)] = ResultsPath, - }; - var filters = new JsonArray(Filters.Length); - foreach (var filter in Filters) - { - filters.Add(filter); - } - jsonObject[nameof(Filters)] = filters; - return jsonObject.ToString(); - } - - internal void Deserialize(string? json) - { - var jsonObject = SimpleJsonSerializer.DeserializeObject(json); - if (jsonObject == null) - return; - - MethodName = (string)jsonObject[nameof(MethodName)]; - PrintSource = (bool)jsonObject[nameof(PrintSource)]; - MaxDepth = Convert.ToInt32(jsonObject[nameof(MaxDepth)]); - Syntax = (string) jsonObject[nameof(Syntax)]; - TargetFrameworkMoniker = (string) jsonObject[nameof(TargetFrameworkMoniker)]; - ResultsPath = (string) jsonObject[nameof(ResultsPath)]; - var filters = (JsonArray) jsonObject[nameof(Filters)]; - Filters = new string[filters.Count]; - for (int i = 0; i < filters.Count; ++i) - { - Filters[i] = (string) filters[i]; - } - } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs index 21d780d6b7..a1b51f9e2b 100644 --- a/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs +++ b/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs @@ -26,7 +26,7 @@ private static ulong GetMinValidAddress() if (OsDetector.IsWindows()) return ushort.MaxValue + 1; if (OsDetector.IsLinux()) - return (ulong) Environment.SystemPageSize; + return (ulong)Environment.SystemPageSize; if (OsDetector.IsMacOS()) return RuntimeInformation.GetCurrentPlatform() switch { @@ -121,8 +121,8 @@ internal DisassemblyResult AttachAndDisassemble(ClrMdArgs args) return new DisassemblyResult { Methods = filteredMethods, - SerializedAddressToNameMapping = state.AddressToNameMapping.Select(x => new DisassemblyResult.MutablePair { Key = x.Key, Value = x.Value }).ToArray(), - PointerSize = (uint) IntPtr.Size + AddressToNameMapping = state.AddressToNameMapping, + PointerSize = (uint)IntPtr.Size }; } @@ -296,7 +296,7 @@ protected void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD, } var method = runtime.GetMethodByInstructionPointer(address); - if (method is null && (address & ((uint) runtime.DataTarget.DataReader.PointerSize - 1)) == 0 + if (method is null && (address & ((uint)runtime.DataTarget.DataReader.PointerSize - 1)) == 0 && runtime.DataTarget.DataReader.ReadPointer(address, out ulong newAddress) && IsValidAddress(newAddress)) { method = runtime.GetMethodByInstructionPointer(newAddress); diff --git a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs index b2c96eb7ff..22b081650e 100644 --- a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs +++ b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs @@ -2,11 +2,14 @@ using Gee.External.Capstone.Arm64; using Iced.Intel; using Microsoft.Diagnostics.Runtime; -using SimpleJson; + using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.ComponentModel; + #if NET6_0_OR_GREATER using System.Diagnostics.CodeAnalysis; @@ -16,29 +19,17 @@ namespace BenchmarkDotNet.Disassemblers; +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(Sharp), typeDiscriminator: nameof(Sharp))] +[JsonDerivedType(typeof(IntelAsm), typeDiscriminator: nameof(IntelAsm))] +[JsonDerivedType(typeof(Arm64Asm), typeDiscriminator: nameof(Arm64Asm))] +[JsonDerivedType(typeof(MonoCode), typeDiscriminator: nameof(MonoCode))] public abstract class SourceCode { // Closed hierarchy. internal SourceCode() { } public ulong InstructionPointer { get; set; } - - internal JsonObject Serialize() - { - var json = new JsonObject { ["$type"] = GetType().Name }; - Serialize(json); - return json; - } - - private protected virtual void Serialize(JsonObject json) - { - json[nameof(InstructionPointer)] = InstructionPointer.ToString(); - } - - internal virtual void Deserialize(JsonObject json) - { - InstructionPointer = ulong.Parse((string) json[nameof(InstructionPointer)]); - } } public sealed class Sharp : SourceCode @@ -46,24 +37,6 @@ public sealed class Sharp : SourceCode public string Text { get; set; } = default!; public string FilePath { get; set; } = default!; public int LineNumber { get; set; } - - private protected override void Serialize(JsonObject json) - { - base.Serialize(json); - - json[nameof(Text)] = Text; - json[nameof(FilePath)] = FilePath; - json[nameof(LineNumber)] = LineNumber; - } - - internal override void Deserialize(JsonObject json) - { - base.Deserialize(json); - - Text = (string) json[nameof(Text)]; - FilePath = (string) json[nameof(FilePath)]; - LineNumber = Convert.ToInt32(json[nameof(LineNumber)]); - } } public abstract class Asm : SourceCode @@ -74,29 +47,6 @@ internal Asm() { } public int InstructionLength { get; set; } public ulong? ReferencedAddress { get; set; } public bool IsReferencedAddressIndirect { get; set; } - - private protected override void Serialize(JsonObject json) - { - base.Serialize(json); - json[nameof(InstructionLength)] = InstructionLength; - if (ReferencedAddress.HasValue) - { - json[nameof(ReferencedAddress)] = ReferencedAddress.ToString(); - } - json[nameof(IsReferencedAddressIndirect)] = IsReferencedAddressIndirect; - } - - internal override void Deserialize(JsonObject json) - { - base.Deserialize(json); - - InstructionLength = Convert.ToInt32(json[nameof(InstructionLength)]); - if (json.TryGetValue(nameof(ReferencedAddress), out var ra)) - { - ReferencedAddress = ulong.Parse((string) ra); - } - IsReferencedAddressIndirect = (bool) json[nameof(IsReferencedAddressIndirect)]; - } } #if NET6_0_OR_GREATER @@ -109,158 +59,131 @@ public sealed class IntelAsm() : Asm public Instruction Instruction { get; set; } public override string ToString() => Instruction.ToString(); +} - private protected override void Serialize(JsonObject json) - { - base.Serialize(json); +public sealed class Arm64Asm : Asm +{ + [JsonInclude] + [EditorBrowsable(EditorBrowsableState.Never)] + internal Arm64AsmData Data { get; set; } - var instructionJson = new JsonObject(); - foreach (var property in typeof(Instruction).GetProperties()) - { - if (property.GetSetMethod() is not null && property.GetGetMethod() is not null) - { - instructionJson[property.Name] = property.GetValue(Instruction) switch - { - ulong l => l.ToString(), - long l => l.ToString(), - Enum e => e.ToString(), - var propertyValue => propertyValue - }; - } - } - json[nameof(Instruction)] = instructionJson; + [JsonIgnore] + public Arm64Instruction? Instruction + { + get => Data.Instruction; + set => Data = Data with { Instruction = value }; } - internal override void Deserialize(JsonObject json) + [JsonIgnore] + internal DisassembleSyntax DisassembleSyntax { - base.Deserialize(json); - - object instruction = new Instruction(); - foreach (var kvp in (JsonObject) json[nameof(Instruction)]) - { - object value = kvp.Value; - var property = typeof(Instruction).GetProperty(kvp.Key)!; - var propertyType = property.PropertyType; - if (propertyType == typeof(ulong)) - { - value = ulong.Parse((string) value); - } - else if (propertyType == typeof(long)) - { - value = long.Parse((string) value); - } - else if (typeof(Enum).IsAssignableFrom(propertyType)) - { - value = Enum.Parse(propertyType, (string) value); - } - else if (propertyType.IsPrimitive) - { - value = Convert.ChangeType(value, propertyType); - } - property.SetValue(instruction, value); - } - Instruction = (Instruction)instruction; + get => Data.DisassembleSyntax; + set => Data = Data with { DisassembleSyntax = value }; } -} - -public sealed class Arm64Asm : Asm -{ - private const string AddressKey = "Arm64Address"; - private const string BytesKey = "Arm64Bytes"; - private const string SyntaxKey = "Arm64Syntax"; - - public Arm64Instruction? Instruction { get; set; } - internal DisassembleSyntax DisassembleSyntax { get; set; } public override string ToString() => Instruction?.ToString() ?? ""; - private protected override void Serialize(JsonObject json) + // Wrapper class to hold Arm64 instruction and disassemble syntax. + [JsonConverter(typeof(Arm64AsmDataConverter))] + internal record struct Arm64AsmData { - base.Serialize(json); + internal DisassembleSyntax DisassembleSyntax { get; set; } - // We only need the address, bytes, and syntax to reconstruct the instruction. - if (Instruction?.Bytes?.Length > 0) - { - json[AddressKey] = Instruction.Address.ToString(); - json[BytesKey] = Convert.ToBase64String(Instruction.Bytes); - json[SyntaxKey] = (int)DisassembleSyntax; - } + public Arm64Instruction? Instruction { get; set; } } - internal override void Deserialize(JsonObject json) + // Custom JsonConverter for Arm64AsmData. + internal class Arm64AsmDataConverter : JsonConverter { - base.Deserialize(json); + private const string Arm64AddressKey = "arm64Address"; + private const string Arm64BytesKey = "arm64Bytes"; + private const string Arm64SyntaxKey = "arm64Syntax"; - if (json.TryGetValue(BytesKey, out var bytes64)) + public override Arm64AsmData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - // Use the Capstone disassembler to recreate the instruction from the bytes. + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException(); + + long instructionAddress = default; + byte[] instructionBytes = []; + DisassembleSyntax syntax = DisassembleSyntax.Masm; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + break; + + if (reader.TokenType != JsonTokenType.PropertyName) + throw new JsonException(); + + string propertyName = reader.GetString()!; + reader.Read(); + + switch (propertyName) + { + case Arm64AddressKey: + instructionAddress = reader.GetInt64(); + break; + + case Arm64BytesKey: + instructionBytes = reader.GetBytesFromBase64(); + break; + + case Arm64SyntaxKey: + var syntaxValue = reader.GetString()!; + syntax = syntaxValue switch + { + "Intel" => DisassembleSyntax.Intel, + "Att" => DisassembleSyntax.Att, + "Masm" => DisassembleSyntax.Masm, + _ => DisassembleSyntax.Masm, + }; + break; + + default: + throw new NotSupportedException($"Unknown property({propertyName}) found."); + } + } + + if (reader.TokenType != JsonTokenType.EndObject) + throw new JsonException("Invalid JSON"); + using var disassembler = CapstoneDisassembler.CreateArm64Disassembler(Arm64DisassembleMode.Arm); disassembler.EnableInstructionDetails = true; - disassembler.DisassembleSyntax = (DisassembleSyntax)Convert.ToInt32(json[SyntaxKey]); - byte[] bytes = Convert.FromBase64String((string)bytes64); - Instruction = disassembler.Disassemble(bytes, long.Parse((string)json[AddressKey])).Single(); + disassembler.DisassembleSyntax = syntax; + var instruction = disassembler.Disassemble(instructionBytes, instructionAddress).SingleOrDefault(); + + return new Arm64AsmData + { + DisassembleSyntax = syntax, + Instruction = instruction, + }; + } + + public override void Write(Utf8JsonWriter writer, Arm64AsmData value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(Arm64SyntaxKey, value.DisassembleSyntax.ToString()); + var instruction = value.Instruction; + if (instruction != null) + { + writer.WriteNumber(Arm64AddressKey, instruction.Address); + writer.WriteBase64String(Arm64BytesKey, instruction.Bytes); + } + writer.WriteEndObject(); } } + } public sealed class MonoCode : SourceCode { public string Text { get; set; } = ""; - - private protected override void Serialize(JsonObject json) - { - base.Serialize(json); - - json[nameof(Text)] = Text; - } - - internal override void Deserialize(JsonObject json) - { - base.Deserialize(json); - - Text = (string) json[nameof(Text)]; - } } public sealed class Map { - [XmlArray("Instructions")] - [XmlArrayItem(nameof(SourceCode), typeof(SourceCode))] - [XmlArrayItem(nameof(Sharp), typeof(Sharp))] - [XmlArrayItem(nameof(IntelAsm), typeof(IntelAsm))] public SourceCode[] SourceCodes { get; set; } = []; - - internal JsonObject Serialize() - { - var sourceCodes = new JsonArray(SourceCodes.Length); - foreach (var sourceCode in SourceCodes) - { - sourceCodes.Add(sourceCode.Serialize()); - } - return new JsonObject - { - [nameof(SourceCodes)] = sourceCodes, - }; - } - - internal void Deserialize(JsonObject json) - { - var sourceCodes = (JsonArray) json[nameof(SourceCodes)]; - SourceCodes = new SourceCode[sourceCodes.Count]; - for (int i = 0; i < sourceCodes.Count; i++) - { - var sourceJson = (JsonObject) sourceCodes[i]; - SourceCodes[i] = sourceJson["$type"] switch - { - nameof(Sharp) => new Sharp(), - nameof(IntelAsm) => new IntelAsm(), - nameof(Arm64Asm) => new Arm64Asm(), - nameof(MonoCode) => new MonoCode(), - var unhandledType => throw new NotSupportedException($"Unexpected type: {unhandledType}") - }; - SourceCodes[i].Deserialize(sourceJson); - } - } } public sealed class DisassembledMethod @@ -282,121 +205,16 @@ public static DisassembledMethod Empty(string fullSignature, ulong nativeCode, s NativeCode = nativeCode, Problem = problem }; - - internal JsonObject Serialize() - { - var maps = new JsonArray(Maps.Length); - foreach (var map in Maps) - { - maps.Add(map.Serialize()); - } - return new JsonObject - { - [nameof(Name)] = Name, - [nameof(NativeCode)] = NativeCode.ToString(), - [nameof(Problem)] = Problem, - [nameof(Maps)] = maps, - [nameof(CommandLine)] = CommandLine - }; - } - - internal void Deserialize(JsonObject json) - { - Name = (string) json[nameof(Name)]; - NativeCode = ulong.Parse((string) json[nameof(NativeCode)]); - Problem = (string) json[nameof(Problem)]; - - var maps = (JsonArray) json[nameof(Maps)]; - Maps = new Map[maps.Count]; - for (int i = 0; i < maps.Count; i++) - { - Maps[i] = new Map(); - Maps[i].Deserialize((JsonObject) maps[i]); - } - - CommandLine = (string) json[nameof(CommandLine)]; - } } public sealed class DisassemblyResult { public DisassembledMethod[] Methods { get; set; } = []; public string[] Errors { get; set; } = []; - public MutablePair[] SerializedAddressToNameMapping { get; set; } = []; - public uint PointerSize { get; set; } - - [XmlIgnore] // XmlSerializer does not support dictionaries ;) - public Dictionary AddressToNameMapping - => _addressToNameMapping ??= SerializedAddressToNameMapping.ToDictionary(x => x.Key, x => x.Value); - [XmlIgnore] - private Dictionary? _addressToNameMapping = null; - - // KeyValuePair is not serializable, because it has read-only properties - // so we need to define our own... - [Serializable] - [XmlType(TypeName = "Workaround")] - public struct MutablePair - { - public ulong Key { get; set; } - public string Value { get; set; } - } - - internal JsonObject Serialize() - { - var methods = new JsonArray(Methods.Length); - foreach (var method in Methods) - { - methods.Add(method.Serialize()); - } - var errors = new JsonArray(Errors.Length); - foreach (var error in Errors) - { - errors.Add(error); - } - var addressToNameMapping = new JsonObject(); - foreach (var kvp in SerializedAddressToNameMapping) - { - addressToNameMapping[kvp.Key.ToString()] = kvp.Value; - } - return new JsonObject - { - [nameof(Methods)] = methods, - [nameof(Errors)] = errors, - [nameof(AddressToNameMapping)] = addressToNameMapping, - [nameof(PointerSize)] = PointerSize.ToString() - }; - } - - internal void Deserialize(JsonObject json) - { - var methods = (JsonArray) json[nameof(Methods)]; - Methods = new DisassembledMethod[methods.Count]; - for (int i = 0; i < methods.Count; i++) - { - Methods[i] = new DisassembledMethod(); - Methods[i].Deserialize((JsonObject) methods[i]); - } - - var errors = (JsonArray) json[nameof(Errors)]; - Errors = new string[errors.Count]; - for (int i = 0; i < errors.Count; i++) - { - Errors[i] = (string) errors[i]; - } - - var addressToNameMapping = (JsonObject) json[nameof(AddressToNameMapping)]; - SerializedAddressToNameMapping = new MutablePair[addressToNameMapping.Count]; - int addressIndex = 0; - foreach (var kvp in addressToNameMapping) - { - SerializedAddressToNameMapping[addressIndex].Key = ulong.Parse(kvp.Key); - SerializedAddressToNameMapping[addressIndex].Value = (string) kvp.Value; - ++addressIndex; - } + public uint PointerSize { get; set; } - PointerSize = uint.Parse((string) json[nameof(PointerSize)]); - } + public Dictionary AddressToNameMapping { get; set; } = []; } public static class DisassemblerConstants @@ -459,7 +277,7 @@ public bool Equals(ClrMethod? x, ClrMethod? y) return x.NativeCode == y.NativeCode; } - public int GetHashCode(ClrMethod obj) => (int) obj.NativeCode; + public int GetHashCode(ClrMethod obj) => (int)obj.NativeCode; } } diff --git a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs index 80c404f5d1..cafa3d04de 100644 --- a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs +++ b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using BenchmarkDotNet.Analysers; +using BenchmarkDotNet.Analysers; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Disassemblers; @@ -11,17 +6,23 @@ using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Portability; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; +using BenchmarkDotNet.Serialization; using BenchmarkDotNet.Toolchains.InProcess.NoEmit; using BenchmarkDotNet.Validators; using JetBrains.Annotations; using Perfolizer.Metrology; -using SimpleJson; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; #nullable enable @@ -234,15 +235,15 @@ InProcessDiagnoserHandlerData IInProcessDiagnoser.GetHandlerData(BenchmarkCase b { return default; } - return new(typeof(DisassemblyDiagnoserInProcessHandler), BuildClrMdArgs(benchmarkCase, "", 0).Serialize()); + + var clrMdArgs = BuildClrMdArgs(benchmarkCase, "", 0); + return new(typeof(DisassemblyDiagnoserInProcessHandler), BdnJsonSerializer.Serialize(clrMdArgs)); } void IInProcessDiagnoser.DeserializeResults(BenchmarkCase benchmarkCase, string results) { - var json = SimpleJsonSerializer.DeserializeObject(results); - var result = new DisassemblyResult(); - result.Deserialize(json); - this.results.Add(benchmarkCase, result); + var disassemblyResult = BdnJsonSerializer.Deserialize(results); + this.results.Add(benchmarkCase, disassemblyResult); } private class NativeCodeSizeMetricDescriptor : IMetricDescriptor @@ -270,7 +271,7 @@ public sealed class DisassemblyDiagnoserInProcessHandler : IInProcessDiagnoserHa void IInProcessDiagnoserHandler.Initialize(string? serializedConfig) { - _clrMdArgs.Deserialize(serializedConfig); + _clrMdArgs = BdnJsonSerializer.Deserialize(serializedConfig!); } void IInProcessDiagnoserHandler.Handle(BenchmarkSignal signal, InProcessDiagnoserActionArgs args) @@ -288,8 +289,7 @@ void IInProcessDiagnoserHandler.Handle(BenchmarkSignal signal, InProcessDiagnose string IInProcessDiagnoserHandler.SerializeResults() { - SimpleJsonSerializer.CurrentJsonSerializerStrategy.Indent = false; - return _result.Serialize().ToString(); + return BdnJsonSerializer.Serialize(_result); } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Serialization/BdnJsonSerializer.cs b/src/BenchmarkDotNet/Serialization/BdnJsonSerializer.cs new file mode 100644 index 0000000000..8ea6ddba04 --- /dev/null +++ b/src/BenchmarkDotNet/Serialization/BdnJsonSerializer.cs @@ -0,0 +1,47 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BenchmarkDotNet.Serialization; + +internal static class BdnJsonSerializer +{ + private static readonly JsonSerializerOptions DefaultOptions = new() + { + Converters = + { + new JsonStringEnumConverter(), + }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + // Disable escaping non ASCII chars. https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/character-encoding#serialize-all-characters + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // TODO: Replace to custom encoder that escape minimal chars. (https://github.com/dotnet/runtime/issues/87153) + NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, // Required to support NaN + PropertyNameCaseInsensitive = true, // Accept PascalNaming that generated by JsonExporter. + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + RespectNullableAnnotations = true, + TypeInfoResolverChain = + { + BdnJsonSerializerContext.Default, + }, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, // Throw error if unknown entry exists. + }; + + private static readonly JsonSerializerOptions IndentedOptions = new(DefaultOptions) + { + WriteIndented = true, + }; + + public static string Serialize(T item, bool indentJson = false) + { + if (indentJson) + return JsonSerializer.Serialize(item, IndentedOptions); + else + return JsonSerializer.Serialize(item, DefaultOptions); + } + + public static T Deserialize(string json) + { + return JsonSerializer.Deserialize(json, DefaultOptions); + } +} diff --git a/src/BenchmarkDotNet/Serialization/BdnJsonSerializerContext.cs b/src/BenchmarkDotNet/Serialization/BdnJsonSerializerContext.cs new file mode 100644 index 0000000000..3a3c2ff2e1 --- /dev/null +++ b/src/BenchmarkDotNet/Serialization/BdnJsonSerializerContext.cs @@ -0,0 +1,16 @@ +using BenchmarkDotNet.Disassemblers; +using System.Text.Json.Serialization; + +namespace BenchmarkDotNet.Serialization; + +[JsonSerializable(typeof(ClrMdArgs))] +[JsonSerializable(typeof(Sharp))] +[JsonSerializable(typeof(MonoCode))] +[JsonSerializable(typeof(IntelAsm))] +[JsonSerializable(typeof(Arm64Asm))] +[JsonSerializable(typeof(Map))] +[JsonSerializable(typeof(DisassembledMethod))] +[JsonSerializable(typeof(DisassemblyResult))] +internal partial class BdnJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/tests/BenchmarkDotNet.IntegrationTests/LargeAddressAwareTest.cs b/tests/BenchmarkDotNet.IntegrationTests/LargeAddressAwareTest.cs index 2fb27498cd..66a056d063 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/LargeAddressAwareTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/LargeAddressAwareTest.cs @@ -24,7 +24,7 @@ public class LargeAddressAwareTest public void BenchmarkCanAllocateMoreThan2Gb_Core() { var platform = RuntimeInformation.GetCurrentPlatform(); - var config = ManualConfig.CreateEmpty(); + var config = ManualConfig.CreateEmpty().WithBuildTimeout(TimeSpan.FromSeconds(240)); // Running 32-bit benchmarks with .Net Core requires passing the path to 32-bit SDK, // which makes this test more complex than it's worth in CI, so we only test 64-bit. config.AddJob(Job.Dry.WithRuntime(CoreRuntime.Core80).WithPlatform(platform).WithId(platform.ToString())); diff --git a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj index ea90fe0e89..0e21034d6c 100755 --- a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj +++ b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs b/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs new file mode 100644 index 0000000000..b305be975c --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs @@ -0,0 +1,261 @@ +using AwesomeAssertions; +using BenchmarkDotNet.Disassemblers; +using BenchmarkDotNet.Serialization; +using BenchmarkDotNet.Tests.XUnit; +using Gee.External.Capstone; +using Gee.External.Capstone.Arm64; +using Iced.Intel; +using System.Linq; +using Xunit; + +namespace BenchmarkDotNet.Tests; + +public class DisassemblerModelSerializationTests +{ + [Fact] + public void ClrMdArgsSerializationTest() + { + // Arrange + var model = new ClrMdArgs( + processId: 100, // ProcessID field is not serialized/deserialized. + typeName: "TypeName", // TypeName field is not serialized/deserialized. + methodName: "MethodName", + printSource: true, + maxDepth: 5, + syntax: "Syntax", + tfm: "Tfm", + filters: ["filter1", "filter2"], + resultsPath: "/path/to/results" + ); + + // Act + var json = BdnJsonSerializer.Serialize(model); + var result = BdnJsonSerializer.Deserialize(json); + + // Assert + json.Should().NotBe("{}"); + result.Should().BeEquivalentTo( + model, + options => options + .IncludingInternalFields() + .ComparingByMembers(typeof(ClrMdArgs)) // Required to use Excluding for struct type. See: https://github.com/fluentassertions/fluentassertions/issues/937 + .Excluding(x => x.ProcessId) + .Excluding(x => x.TypeName)); + } + + [Fact] + public void SharpSerializationTest() + { + var model = new Sharp + { + InstructionPointer = 1, + Text = "Text", + FilePath = "FilePath", + LineNumber = 2, + }; + + // Act + var json = BdnJsonSerializer.Serialize(model); + var result = BdnJsonSerializer.Deserialize(json); + + // Assert + json.Should().NotBe("{}"); + result.Should().BeEquivalentTo(model); + } + + [Fact] + public void MonoCodeSerializationTest() + { + // Arrange + var model = new MonoCode + { + InstructionPointer = 1, + Text = "Text", + }; + + // Act + var json = BdnJsonSerializer.Serialize(model); + var result = BdnJsonSerializer.Deserialize(json); + + // Assert + json.Should().NotBe("{}"); + result.Should().BeEquivalentTo(model); + } + + [Fact] + public void IntelAsmSerializationTest() + { + // Arrange + var model = new IntelAsm + { + InstructionPointer = 1, + InstructionLength = 2, + ReferencedAddress = 3, + IsReferencedAddressIndirect = true, + Instruction = Instruction.Create( + Iced.Intel.Code.Xlat_m8, + new MemoryOperand(Register.RBX, Register.AL) + ), + }; + + // Act + var json = BdnJsonSerializer.Serialize(model); + var result = BdnJsonSerializer.Deserialize(json); + + // Assert + Assert.NotEqual("{}", json); + Assert.Equivalent(model, result, strict: true); + } + + [FactEnvSpecific("ARM64 disassembler is not supported on .NET Framework or Windows+Arm environment", EnvRequirement.NonFullFramework, EnvRequirement.NonWindowsArm)] + public void Arm64AsmSerializationTest() + { + // Arrange + byte[] instructionBytes = [0xE1, 0x0B, 0x40, 0xB9]; // ldr w1, [sp, #8] + var disassembleSyntax = DisassembleSyntax.Intel; + + // Create instruction instance by using disassembler. + using var disassembler = CapstoneDisassembler.CreateArm64Disassembler(Arm64DisassembleMode.Arm); + disassembler.EnableInstructionDetails = true; + disassembler.DisassembleSyntax = disassembleSyntax; + + // Act + var instructions = disassembler.Disassemble(instructionBytes); + var instruction = instructions.Single(); + + var model = new Arm64Asm + { + DisassembleSyntax = disassembleSyntax, + Instruction = instruction, + InstructionLength = instruction.Bytes.Length, + InstructionPointer = (ulong)instruction.Address, + ReferencedAddress = (instruction.Address > ushort.MaxValue) ? (ulong)instruction.Address : null, + IsReferencedAddressIndirect = true, // Test with dummy value + }; + + // Act + var json = BdnJsonSerializer.Serialize(model); + var result = BdnJsonSerializer.Deserialize(json); + + // Assert + json.Should().NotBe("{}"); + + // Compare properties (Except for`Instruction.Details.Operands` property that ) + result.Instruction.ToString().Should().Be("ldr w1, [sp, #8]"); + + result.Should() + .BeEquivalentTo(model, options => options.Excluding(x => x.Instruction.Details.Operands)); + } + + [Fact] + public void MapSerializationTest() + { + // Arrange + var model = new Map + { + SourceCodes = + [ + new MonoCode + { + Text = "MonoCodeText1", + InstructionPointer = 1, + }, + new Sharp + { + Text = "SharpText" , + FilePath ="FilePath", + LineNumber = 1, + InstructionPointer = 2, + }, + new MonoCode { + Text = "MonoCodeText2", + InstructionPointer = 2, + }, + ] + }; + + // Act + var json = BdnJsonSerializer.Serialize(model); + var result = BdnJsonSerializer.Deserialize(json); + + // Assert + json.Should().NotBe("{}"); + result.Should().BeEquivalentTo(model); + } + + [Fact] + public void DisassembledMethodSerializationTest() + { + // Arrange + var model = new DisassembledMethod + { + Name = "Name", + CommandLine = "CommandLine", + NativeCode = 1, + Problem = "Problem", + Maps = + [ + new Map() + { + SourceCodes = + [ + new MonoCode + { + InstructionPointer = 1, + Text = "MonoCode1", + }, + ] + }, + new Map() + { + SourceCodes = + [ + new MonoCode + { + InstructionPointer = 2, + Text = "MonoCode2", + }, + ] + }, + ] + }; + + // Act + var json = BdnJsonSerializer.Serialize(model); + var result = BdnJsonSerializer.Deserialize(json); + + // Assert + json.Should().NotBe("{}"); + result.Should().BeEquivalentTo(model); + } + + [Fact] + public void DisassemblyResultSerializationTest() + { + var model = new DisassemblyResult + { + AddressToNameMapping = + { + [1] = "Name1", + [2] = "Name2", + [3] = "Name3", + }, + Errors = ["Error1", "Error2", "Error3"], + Methods = + [ + new DisassembledMethod{Name= "Method1" }, + new DisassembledMethod{Name= "Method2" }, + new DisassembledMethod{Name= "Method3" }, + ], + PointerSize = 1, + }; + + // Act + var json = BdnJsonSerializer.Serialize(model); + var result = BdnJsonSerializer.Deserialize(json); + + // Assert + Assert.NotEqual("{}", json); + Assert.Equivalent(model, result, strict: true); + } +}