Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add System.Text.Json to JSON serializer benchmarks #2740

Merged
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
7 changes: 4 additions & 3 deletions src/benchmarks/micro/MicroBenchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
<DebugType>portable</DebugType>
<DebugSymbols>true</DebugSymbols>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableXlfLocalization>false</EnableXlfLocalization>
<!-- this repo needs to build with netcoreapp3.1, so we can't use latest C# syntax -->
<LangVersion>8.0</LangVersion>
<EnableXlfLocalization>false</EnableXlfLocalization>
<LangVersion>9.0</LangVersion>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that source gen requires C# 9 since it is setting init-only properties. I've conditioned the lang version on the target framework as it seemed like the smallest possible fix, but I defer to maintainers on the right approach.

cc @adamsitnik

eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved

<!-- Allow building with one major version, and running using a sdk with a higher major version -->
<RollForward Condition="'$(BuildingForWasm)' == 'true'">LatestMajor</RollForward>
Expand All @@ -27,6 +26,8 @@
<PropertyGroup>
<ExtensionsVersion>3.1.22</ExtensionsVersion>
<SystemVersion>4.7.1</SystemVersion>
<!-- this repo needs to build with netcoreapp3.1, so we can't use latest C# syntax -->
<LangVersion>8.0</LangVersion>
</PropertyGroup>
</When>
<When Condition="'$(TargetFramework)' == 'net6.0'">
Expand Down
58 changes: 58 additions & 0 deletions src/benchmarks/micro/Serializers/DataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
#if NET6_0_OR_GREATER
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
#endif
using System.Xml;
using System.Xml.Serialization;
using BenchmarkDotNet.Extensions;
Expand Down Expand Up @@ -215,6 +220,21 @@ private static XmlElement CreateXmlElement()
xmlElement.InnerText = "Element innertext";
return xmlElement;
}

internal static JsonSerializerOptions GetJsonSerializerOptions(SystemTextJsonSerializationMode mode)
=> mode switch
{
SystemTextJsonSerializationMode.Reflection => new JsonSerializerOptions(),
#if NET6_0_OR_GREATER
SystemTextJsonSerializationMode.SourceGen => SystemTextJsonSourceGeneratedContext.Default.Options,
#endif
_ => throw new NotSupportedException(mode.ToString())
};

#if NET6_0_OR_GREATER
internal static JsonTypeInfo<T> GetSystemTextJsonSourceGenMetadata<T>()
=> (JsonTypeInfo<T>)SystemTextJsonSourceGeneratedContext.Default.GetTypeInfo(typeof(T));
#endif
}

// the view models come from a real world app called "AllReady"
Expand Down Expand Up @@ -421,4 +441,42 @@ public void WriteXml(XmlWriter writer)
writer.WriteAttributeString("BoolValue", BoolValue.ToString());
}
}

public class ClassWithObjectProperty
{
public object Prop { get; set; }
}

public enum SystemTextJsonSerializationMode
{
Reflection = 0,
SourceGen = 1
}

#if NET6_0_OR_GREATER
[JsonSerializable(typeof(ClassWithObjectProperty))]
[JsonSerializable(typeof(LoginViewModel))]
[JsonSerializable(typeof(Location))]
[JsonSerializable(typeof(IndexViewModel))]
[JsonSerializable(typeof(MyEventsListerViewModel))]
[JsonSerializable(typeof(BinaryData))]
[JsonSerializable(typeof(CollectionsOfPrimitives))]
[JsonSerializable(typeof(XmlElement))]
[JsonSerializable(typeof(SimpleStructWithProperties))]
[JsonSerializable(typeof(SimpleListOfInt))]
[JsonSerializable(typeof(ClassImplementingIXmlSerialiable))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(ImmutableDictionary<string, string>))]
[JsonSerializable(typeof(ImmutableSortedDictionary<string, string>))]
[JsonSerializable(typeof(HashSet<string>))]
[JsonSerializable(typeof(ArrayList))]
[JsonSerializable(typeof(Hashtable))]
[JsonSerializable(typeof(LargeStructWithProperties))]
[JsonSerializable(typeof(DateTimeOffset?))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(object))]
internal partial class SystemTextJsonSourceGeneratedContext : JsonSerializerContext
{
}
#endif
}
47 changes: 45 additions & 2 deletions src/benchmarks/micro/Serializers/Json_FromStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Filters;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Filters;

namespace MicroBenchmarks.Serializers
{
Expand Down Expand Up @@ -117,6 +117,49 @@ public T DataContractJsonSerializer_()
return (T)dataContractJsonSerializer.ReadObject(memoryStream);
}

#if NET6_0_OR_GREATER
[GlobalSetup(Target = nameof(SystemTextJson_Reflection_))]
public void SetupSystemTextJson_Reflection_()
{
value = DataGenerator.Generate<T>();

// the stream is pre-allocated, we don't want the benchmarks to include stream allocaton cost
memoryStream = new MemoryStream(capacity: short.MaxValue);
memoryStream.Position = 0;
System.Text.Json.JsonSerializer.Serialize(memoryStream, value);
}

[BenchmarkCategory(Categories.Runtime, Categories.Libraries)]
[Benchmark(Description = "SystemTextJson_Reflection")]
public T SystemTextJson_Reflection_()
{
memoryStream.Position = 0;
return System.Text.Json.JsonSerializer.Deserialize<T>(memoryStream);
}

private System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> sourceGenMetadata;

[GlobalSetup(Target = nameof(SystemTextJson_SourceGen_))]
public void SetupSystemTextJson_SourceGen_()
{
value = DataGenerator.Generate<T>();
sourceGenMetadata = DataGenerator.GetSystemTextJsonSourceGenMetadata<T>();

// the stream is pre-allocated, we don't want the benchmarks to include stream allocaton cost
memoryStream = new MemoryStream(capacity: short.MaxValue);
memoryStream.Position = 0;
System.Text.Json.JsonSerializer.Serialize(memoryStream, value, sourceGenMetadata);
}

[BenchmarkCategory(Categories.Runtime, Categories.Libraries)]
[Benchmark(Description = "SystemTextJson_SourceGen")]
public T SystemTextJson_SourceGen_()
{
memoryStream.Position = 0;
return System.Text.Json.JsonSerializer.Deserialize(memoryStream, sourceGenMetadata);
}
#endif

[GlobalCleanup]
public void Cleanup() => memoryStream.Dispose();

Expand Down
22 changes: 22 additions & 0 deletions src/benchmarks/micro/Serializers/Json_FromString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,27 @@ public class Json_FromString<T>
[BenchmarkCategory(Categories.ThirdParty)]
[Benchmark(Description = "Utf8Json")]
public T Utf8Json_() => Utf8Json.JsonSerializer.Deserialize<T>(serialized);

[GlobalSetup(Target = nameof(SystemTextJson_Reflection_))]
public void SetupSystemTextJson_Reflection_() => serialized = System.Text.Json.JsonSerializer.Serialize(DataGenerator.Generate<T>());

[BenchmarkCategory(Categories.Runtime, Categories.Libraries)]
[Benchmark(Description = "SystemTextJson_Reflection")]
public T SystemTextJson_Reflection_() => System.Text.Json.JsonSerializer.Deserialize<T>(serialized);

#if NET6_0_OR_GREATER
private System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> sourceGenMetadata;

[GlobalSetup(Target = nameof(SystemTextJson_SourceGen_))]
public void SetupSystemTextJson_SourceGen_()
{
sourceGenMetadata = DataGenerator.GetSystemTextJsonSourceGenMetadata<T>();
serialized = System.Text.Json.JsonSerializer.Serialize(DataGenerator.Generate<T>(), sourceGenMetadata);
}

[BenchmarkCategory(Categories.Runtime, Categories.Libraries)]
[Benchmark(Description = "SystemTextJson_SourceGen")]
public T SystemTextJson_SourceGen_() => System.Text.Json.JsonSerializer.Deserialize(serialized, sourceGenMetadata);
#endif
}
}
41 changes: 40 additions & 1 deletion src/benchmarks/micro/Serializers/Json_ToStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,49 @@ public void DataContractJsonSerializer_()
dataContractJsonSerializer.WriteObject(memoryStream, value);
}

#if NET6_0_OR_GREATER
[GlobalSetup(Target = nameof(SystemTextJson_Reflection_))]
public void SetupSystemTextJson_Reflection_()
{
value = DataGenerator.Generate<T>();

// the stream is pre-allocated, we don't want the benchmarks to include stream allocaton cost
memoryStream = new MemoryStream(capacity: short.MaxValue);
}

[BenchmarkCategory(Categories.Runtime, Categories.Libraries)]
[Benchmark(Description = "SystemTextJson_Reflection")]
public void SystemTextJson_Reflection_()
{
memoryStream.Position = 0;
System.Text.Json.JsonSerializer.Serialize(memoryStream, value);
}

private System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> sourceGenMetadata;

[GlobalSetup(Target = nameof(SystemTextJson_SourceGen_))]
public void SetupSystemTextJson_SourceGen_()
{
value = DataGenerator.Generate<T>();
sourceGenMetadata = DataGenerator.GetSystemTextJsonSourceGenMetadata<T>();

// the stream is pre-allocated, we don't want the benchmarks to include stream allocaton cost
memoryStream = new MemoryStream(capacity: short.MaxValue);
}

[BenchmarkCategory(Categories.Runtime, Categories.Libraries)]
[Benchmark(Description = "SystemTextJson_SourceGen")]
public void SystemTextJson_SourceGen_()
{
memoryStream.Position = 0;
System.Text.Json.JsonSerializer.Serialize(memoryStream, value, sourceGenMetadata);
}
#endif

[GlobalCleanup]
public void Cleanup()
{
streamWriter.Dispose();
streamWriter?.Dispose();
memoryStream.Dispose();
}
}
Expand Down
21 changes: 20 additions & 1 deletion src/benchmarks/micro/Serializers/Json_ToString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ namespace MicroBenchmarks.Serializers
public class Json_ToString<T>
{
private T value;
#if NET6_0_OR_GREATER
private System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> sourceGenMetadata;
#endif

[GlobalSetup]
public void Setup() => value = DataGenerator.Generate<T>();
public void Setup()
{
value = DataGenerator.Generate<T>();
#if NET6_0_OR_GREATER
sourceGenMetadata = DataGenerator.GetSystemTextJsonSourceGenMetadata<T>();
#endif
}

[BenchmarkCategory(Categories.ThirdParty)]
[Benchmark(Description = "Jil")]
Expand All @@ -35,5 +44,15 @@ public class Json_ToString<T>

// DataContractJsonSerializer does not provide an API to serialize to string
// so it's not included here (apples vs apples thing)

[BenchmarkCategory(Categories.Runtime, Categories.Libraries)]
[Benchmark(Description = "SystemTextJson_Reflection")]
public string SystemTextJson_Reflection_() => System.Text.Json.JsonSerializer.Serialize(value);

#if NET6_0_OR_GREATER
[BenchmarkCategory(Categories.Runtime, Categories.Libraries)]
[Benchmark(Description = "SystemTextJson_SourceGen")]
public string SystemTextJson_SourceGen_() => System.Text.Json.JsonSerializer.Serialize(value, sourceGenMetadata);
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ private T RoundtripSerialization(JsonSerializerOptions options)
[Benchmark]
public T NewDefaultOptions() => RoundtripSerialization(new JsonSerializerOptions());

#if NET6_0_OR_GREATER
[Benchmark]
public T CachedJsonSerializerContext()
=> RoundtripSerialization(SystemTextJsonSourceGeneratedContext.Default.Options);

[Benchmark]
public T NewJsonSerializerContext()
{
var options = new JsonSerializerOptions();
options.AddContext<SystemTextJsonSourceGeneratedContext>();
return RoundtripSerialization(options);
}
#endif

[Benchmark]
public T NewCustomizedOptions() =>
RoundtripSerialization(
Expand All @@ -61,11 +75,11 @@ public T NewCustomConverter() =>

[Benchmark]
public T NewCachedCustomConverter() =>
RoundtripSerialization(
new JsonSerializerOptions
{
WriteIndented = true,
Converters = { _converter }
});
RoundtripSerialization(
new JsonSerializerOptions
{
WriteIndented = true,
Converters = { _converter }
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ namespace System.Text.Json.Serialization.Tests
[AotFilter("Currently not supported due to missing metadata.")]
public class ReadJson<T>
{
#if NET6_0_OR_GREATER
[Params(SystemTextJsonSerializationMode.Reflection, SystemTextJsonSerializationMode.SourceGen)]
#else
[Params(SystemTextJsonSerializationMode.Reflection)]
#endif
public SystemTextJsonSerializationMode Mode;
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved

private JsonSerializerOptions _options;
private string _serialized;
private byte[] _utf8Serialized;
private MemoryStream _memoryStream;
Expand All @@ -40,37 +48,38 @@ public class ReadJson<T>
public async Task Setup()
{
T value = DataGenerator.Generate<T>();
_options = DataGenerator.GetJsonSerializerOptions(Mode);

_serialized = JsonSerializer.Serialize(value);
_serialized = JsonSerializer.Serialize(value, _options);

_utf8Serialized = Encoding.UTF8.GetBytes(_serialized);

_memoryStream = new MemoryStream(capacity: short.MaxValue);
await JsonSerializer.SerializeAsync(_memoryStream, value);
await JsonSerializer.SerializeAsync(_memoryStream, value, _options);
}

[BenchmarkCategory(Categories.Libraries, Categories.JSON)]
[Benchmark]
public T DeserializeFromString() => JsonSerializer.Deserialize<T>(_serialized);
public T DeserializeFromString() => JsonSerializer.Deserialize<T>(_serialized, _options);

[BenchmarkCategory(Categories.Libraries, Categories.JSON)]
[Benchmark]
public T DeserializeFromUtf8Bytes() => JsonSerializer.Deserialize<T>(_utf8Serialized);
public T DeserializeFromUtf8Bytes() => JsonSerializer.Deserialize<T>(_utf8Serialized, _options);

[BenchmarkCategory(Categories.Libraries, Categories.JSON)]
[Benchmark]
public T DeserializeFromReader()
{
Utf8JsonReader reader = new Utf8JsonReader(_utf8Serialized);
return JsonSerializer.Deserialize<T>(ref reader);
return JsonSerializer.Deserialize<T>(ref reader, _options);
}

[BenchmarkCategory(Categories.Libraries, Categories.JSON, Categories.NoWASM)]
[Benchmark]
public async Task<T> DeserializeFromStream()
{
_memoryStream.Position = 0;
T value = await JsonSerializer.DeserializeAsync<T>(_memoryStream);
T value = await JsonSerializer.DeserializeAsync<T>(_memoryStream, _options);
return value;
}

Expand Down
Loading