Skip to content

Commit

Permalink
Add StringBuilder pooling in NewtonsoftJsonSerializer (#4929)
Browse files Browse the repository at this point in the history
* Add StringBuilder pooling in NewtonsoftJsonSerializer

* Fix CreateInternalSettings to properly wire everything up and be harder to goof up later by making static, fix hocon reading

* don't make two settings.

* Fixes for thread safety, fix using scopes.

* api approvals, chars are not bytes so lets not confuse ourselves in HOCON

* fix api approval

Co-authored-by: Aaron Stannard <aaron@petabridge.com>
  • Loading branch information
to11mtm and Aaronontheweb authored Dec 14, 2021
1 parent 12c26c2 commit a9859db
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 12 deletions.
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" />
<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());
}
}
finally
{
if (sb != null)
{
_sbPool.Return(sb);
}
}
}

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

0 comments on commit a9859db

Please sign in to comment.