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 StringBuilder pooling in NewtonsoftJsonSerializer #4929

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
82 changes: 81 additions & 1 deletion src/benchmark/SerializationBenchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,100 @@
//-----------------------------------------------------------------------

using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Akka.Actor;
using Akka.Configuration;
using Akka.Serialization;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Newtonsoft.Json;

namespace SerializationBenchmarks
{
class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<SerializationTests>();
BenchmarkRunner.Run<JsonSerializerTests>();
}
}

public class TestSer
{
public int Id { get; set; }
public string someStr { get; set; }
public string someStr2 { get; set; }
public string someStr3 { get; set; }
public Guid IDK { get; set; }
}
[MemoryDiagnoser]
public class JsonSerializerTests
{
public JsonSerializerTests()
{
_sys_noPool = ActorSystem.Create("bench-serialization-json-nopool",ConfigurationFactory.ParseString(@"
akka.actor {{

serialization-settings {{
json {{
use-pooled-string-builder = false
}}
}}
}}"));
_sys_pool = ActorSystem.Create("bench-serialization-json-pool");
_noPoolSer =
_sys_noPool.Serialization.FindSerializerForType(typeof(object));
_poolSer =
_sys_pool.Serialization.FindSerializerForType(typeof(object));
}
private static TestSer testObj = new TestSer()
{
Id = 124,
someStr =
"412tgieoargj4a9349u2u-03jf3290rjf2390ja209fj1099u42n0f92qm93df3m-032jfq-102",
someStr2 =
"412tgieoargj4a9349u2u-03jf3290rjf2390ja209fj1099u42n0f92qm93df3m-032jfq-102",
someStr3 =
new string(Enumerable.Repeat('l',512).ToArray()),
IDK = Guid.Empty
};

private ActorSystem _sys_noPool;
private ActorSystem _sys_pool;
private Serializer _noPoolSer;
private Serializer _poolSer;
private const int _numIters = 10000;
[Benchmark]
public void Pooling()
{
for (int i = 0; i < _numIters; i++)
{
_poolSer.ToBinary(testObj);
}
}
[Benchmark]
public void NoPooling()
{
for (int i = 0; i < _numIters; i++)
{
_noPoolSer.ToBinary(testObj);
}
}

[Benchmark]
public void Pooling_MultiTasks()
{
Task.WaitAll(Enumerable.Repeat(0, 10)
.Select((l) => Task.Run(Pooling)).ToArray());
}
[Benchmark]
public void NoPooling_MultiTasks()
{
Task.WaitAll(Enumerable.Repeat(0, 10)
.Select((l) => Task.Run(NoPooling)).ToArray());
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4754,10 +4754,13 @@ namespace Akka.Serialization
public sealed class NewtonSoftJsonSerializerSettings
{
public static readonly Akka.Serialization.NewtonSoftJsonSerializerSettings Default;
public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjectReferences, System.Collections.Generic.IEnumerable<System.Type> converters) { }
public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjectReferences, System.Collections.Generic.IEnumerable<System.Type> converters, bool usePooledStringBuilder, int stringBuilderMinSize, int stringBuilderMaxSize) { }
public System.Collections.Generic.IEnumerable<System.Type> Converters { get; }
public bool EncodeTypeNames { get; }
public bool PreserveObjectReferences { get; }
public int StringBuilderMaxSize { get; }
public int StringBuilderMinSize { get; }
public bool UsePooledStringBuilder { get; }
public static Akka.Serialization.NewtonSoftJsonSerializerSettings Create(Akka.Configuration.Config config) { }
}
public sealed class NewtonSoftJsonSerializerSetup : Akka.Actor.Setup.Setup
Expand Down
1 change: 1 addition & 0 deletions src/core/Akka/Akka.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.5" />
Copy link
Member

Choose a reason for hiding this comment

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

At some point this will get folded into the core runtime when we move on from .NET Standard 2.0, IIRC

<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
Expand Down
26 changes: 23 additions & 3 deletions src/core/Akka/Configuration/Pigeon.conf
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,29 @@ akka {
}

# extra settings that can be custom to a serializer implementation
serialization-settings {

}
serialization-settings {
json {

# Used to set whether to use stringbuilders from a pool
# In memory constrained conditions (i.e. IOT)
# You may wish to turn this off
use-pooled-string-builder = true

# The starting size of stringbuilders created in pool
# if use-pooled-string-builder is true.
# You may wish to adjust this number,
# For example if you are confident your messages are smaller or larger
pooled-string-builder-minsize = 2048

# The maximum retained size of a pooled stringbuilder
# if use-pooled-string-builder is true.
# You may wish to turn this number up if your messages are larger
# But do keep in mind that strings in .NET are UTF-16,
# So after ~42k characters you might wind up
# on the Large Object Heap (which may not be a bad thing...)
pooled-string-builder-maxsize = 32768
}
}
}

# Used to set the behavior of the scheduler.
Expand Down
96 changes: 89 additions & 7 deletions src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Akka.Actor;
using Akka.Configuration;
using Akka.Util;
using Microsoft.Extensions.ObjectPool;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
Expand All @@ -32,7 +34,10 @@ public sealed class NewtonSoftJsonSerializerSettings
public static readonly NewtonSoftJsonSerializerSettings Default = new NewtonSoftJsonSerializerSettings(
encodeTypeNames: true,
preserveObjectReferences: true,
converters: Enumerable.Empty<Type>());
converters: Enumerable.Empty<Type>(),
usePooledStringBuilder:true,
stringBuilderMinSize:2048,
stringBuilderMaxSize:32768);

/// <summary>
/// Creates a new instance of the <see cref="NewtonSoftJsonSerializerSettings"/> based on a provided <paramref name="config"/>.
Expand All @@ -52,8 +57,15 @@ public static NewtonSoftJsonSerializerSettings Create(Config config)

return new NewtonSoftJsonSerializerSettings(
encodeTypeNames: config.GetBoolean("encode-type-names", true),
preserveObjectReferences: config.GetBoolean("preserve-object-references", true),
converters: GetConverterTypes(config));
preserveObjectReferences: config.GetBoolean(
"preserve-object-references", true),
converters: GetConverterTypes(config),
usePooledStringBuilder: config.GetBoolean("use-pooled-string-builder", true),
stringBuilderMinSize:config.GetInt("pooled-string-builder-minsize", 2048),
stringBuilderMaxSize:
config.GetInt("pooled-string-builder-maxsize",
32768)
);
}

private static IEnumerable<Type> GetConverterTypes(Config config)
Expand Down Expand Up @@ -89,21 +101,40 @@ private static IEnumerable<Type> GetConverterTypes(Config config)
/// Converters must inherit from <see cref="JsonConverter"/> class and implement a default constructor.
/// </summary>
public IEnumerable<Type> Converters { get; }

/// <summary>
/// The Starting size used for Pooled StringBuilders, if <see cref="UsePooledStringBuilder"/> is -true-
/// </summary>
public int StringBuilderMinSize { get; }
/// <summary>
/// The Max Retained size for Pooled StringBuilders, if <see cref="UsePooledStringBuilder"/> is -true-
/// </summary>
public int StringBuilderMaxSize { get; }
/// <summary>
/// If -true-, Stringbuilders are pooled and reused for serialization to lower memory pressure.
/// </summary>
public bool UsePooledStringBuilder { get; }

/// <summary>
/// Creates a new instance of the <see cref="NewtonSoftJsonSerializerSettings"/>.
/// </summary>
/// <param name="encodeTypeNames">Determines if a special `$type` field should be emitted into serialized JSON. Must be true if corresponding serializer is used as default.</param>
/// <param name="preserveObjectReferences">Determines if object references should be tracked within serialized object graph. Must be true if corresponding serialize is used as default.</param>
/// <param name="converters">A list of types implementing a <see cref="JsonConverter"/> to support custom types serialization.</param>
public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjectReferences, IEnumerable<Type> converters)
/// <param name="usePooledStringBuilder">Determines if string builders will be used from a pool to lower memory usage</param>
/// <param name="stringBuilderMinSize">Starting size used for pooled string builders if enabled</param>
/// <param name="stringBuilderMaxSize">Max retained size used for pooled string builders if enabled</param>
public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjectReferences, IEnumerable<Type> converters, bool usePooledStringBuilder, int stringBuilderMinSize, int stringBuilderMaxSize)
{
if (converters == null)
throw new ArgumentNullException(nameof(converters), $"{nameof(NewtonSoftJsonSerializerSettings)} requires a sequence of converters.");

EncodeTypeNames = encodeTypeNames;
PreserveObjectReferences = preserveObjectReferences;
Converters = converters;
UsePooledStringBuilder = usePooledStringBuilder;
StringBuilderMinSize = stringBuilderMinSize;
StringBuilderMaxSize = stringBuilderMaxSize;
}
}

Expand All @@ -114,7 +145,8 @@ public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjec
public class NewtonSoftJsonSerializer : Serializer
{
private readonly JsonSerializer _serializer;


private readonly ObjectPool<StringBuilder> _sbPool;
/// <summary>
/// TBD
/// </summary>
Expand All @@ -138,11 +170,15 @@ public NewtonSoftJsonSerializer(ExtendedActorSystem system, Config config)
: this(system, NewtonSoftJsonSerializerSettings.Create(config))
{
}



public NewtonSoftJsonSerializer(ExtendedActorSystem system, NewtonSoftJsonSerializerSettings settings)
: base(system)
{
if (settings.UsePooledStringBuilder)
{
_sbPool = new DefaultObjectPoolProvider()
.CreateStringBuilderPool(settings.StringBuilderMinSize,settings.StringBuilderMaxSize);
}
Settings = new JsonSerializerSettings
{
PreserveReferencesHandling = settings.PreserveObjectReferences
Expand Down Expand Up @@ -183,6 +219,8 @@ public NewtonSoftJsonSerializer(ExtendedActorSystem system, NewtonSoftJsonSerial
_serializer = JsonSerializer.Create(Settings);
}



private static JsonConverter CreateConverter(Type converterType, ExtendedActorSystem actorSystem)
{
var ctor = converterType.GetConstructors()
Expand Down Expand Up @@ -228,12 +266,56 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ
/// <param name="obj">The object to serialize </param>
/// <returns>A byte array containing the serialized object</returns>
public override byte[] ToBinary(object obj)
{
if (_sbPool != null)
{
return toBinary_PooledBuilder(obj);
}
else
{
return toBinary_NewBuilder(obj);
}

}

private byte[] toBinary_NewBuilder(object obj)
{
string data = JsonConvert.SerializeObject(obj, Formatting.None, Settings);
byte[] bytes = Encoding.UTF8.GetBytes(data);
return bytes;
}

private byte[] toBinary_PooledBuilder(object obj)
{
//Don't try to opt with
//StringBuilder sb = _sbPool.Get()
//Or removing null check
//Both are necessary to avoid leaking on thread aborts etc
StringBuilder sb = null;
try
{
sb = _sbPool.Get();

using (var tw = new StringWriter(sb, CultureInfo.InvariantCulture))
{
var ser = JsonSerializer.CreateDefault(Settings);
ser.Formatting = Formatting.None;
using (var jw = new JsonTextWriter(tw))
{
ser.Serialize(jw, obj);
}
return Encoding.UTF8.GetBytes(tw.ToString());
Copy link
Member

Choose a reason for hiding this comment

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

LGTM

}
}
finally
{
if (sb != null)
{
_sbPool.Return(sb);
}
}
}

/// <summary>
/// Deserializes a byte array into an object of type <paramref name="type"/>.
/// </summary>
Expand Down