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 2 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;

[Benchmark]
public void Pooling()
{
for (int i = 0; i < 10000; i++)
{
_poolSer.ToBinary(testObj);
}
}
[Benchmark]
public void NoPooling()
{
for (int i = 0; i < 10000; 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
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" />
</ItemGroup>
Expand Down
25 changes: 22 additions & 3 deletions src/core/Akka/Configuration/Pigeon.conf
Original file line number Diff line number Diff line change
Expand Up @@ -522,9 +522,28 @@ 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 = 2048b

# 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 around 85K you'll wind up
# on the Large Object Heap (which may not be a bad thing...)
pooled-string-builder-maxsize = 32768b
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to make sure to document the settings here. I would love some feedback on the defaults, I'm not sure if maxsize is too low or not. I think MinSize is fair here; I tried 1024B and the benchmarks came out about the same.

}
}

# Used to set the behavior of the scheduler.
Expand Down
133 changes: 115 additions & 18 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,17 @@ 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),
config.GetBoolean("use-pooled-string-builder", true),
(int)Math.Min(
config.GetByteSize("pooled-string-builder-minsize", 2048) ??
2048, int.MaxValue),
(int)Math.Min(
config.GetByteSize("pooled-string-builder-maxsize",
32768) ?? 32768, int.MaxValue)
);
}

private static IEnumerable<Type> GetConverterTypes(Config config)
Expand Down Expand Up @@ -89,21 +103,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 +147,8 @@ public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjec
public class NewtonSoftJsonSerializer : Serializer
{
private readonly JsonSerializer _serializer;

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



public NewtonSoftJsonSerializer(ExtendedActorSystem system, NewtonSoftJsonSerializerSettings settings)
: base(system)
{
Settings = new JsonSerializerSettings
if (settings.UsePooledStringBuilder)
{
_sbPool = new DefaultObjectPool<StringBuilder>(
new StringBuilderPooledObjectPolicy()
{
InitialCapacity = settings.StringBuilderMinSize,
MaximumRetainedCapacity =
settings.StringBuilderMaxSize
});
}
Settings = CreateInternalSettings(system, settings,this);
var settingsNoFormat = CreateInternalSettings(system, settings,this);
settingsNoFormat.Formatting = Formatting.None;
_serializer = JsonSerializer.Create(Settings);
_formattingNoneSerializer = JsonSerializer.Create(settingsNoFormat);
Copy link
Member Author

Choose a reason for hiding this comment

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

so, the idea here is to make sure that even if the user wants formatting for -their- serializer, we have our own with no formatting, so that we avoid making a new serializer every time.

}

private static JsonSerializerSettings CreateInternalSettings(
ExtendedActorSystem system, NewtonSoftJsonSerializerSettings settings, NewtonSoftJsonSerializer surrogateParent)
Copy link
Member Author

Choose a reason for hiding this comment

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

I made this static because it makes it harder for people to accidentally miswire things (i.e. by accidentally applying to Settings on the instance when each created JsonSerializerSettings needs to have it's inits run.)

{
var newSettings = new JsonSerializerSettings
{
PreserveReferencesHandling = settings.PreserveObjectReferences
? PreserveReferencesHandling.Objects
: PreserveReferencesHandling.None,
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Ignore,
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
ConstructorHandling =
ConstructorHandling.AllowNonPublicDefaultConstructor,
TypeNameHandling = settings.EncodeTypeNames
? TypeNameHandling.All
: TypeNameHandling.None,
};

if (system != null)
{
var settingsSetup = system.Settings.Setup.Get<NewtonSoftJsonSerializerSetup>()
.GetOrElse(NewtonSoftJsonSerializerSetup.Create(s => {}));
var settingsSetup = system.Settings.Setup
.Get<NewtonSoftJsonSerializerSetup>()
.GetOrElse(NewtonSoftJsonSerializerSetup.Create(s => { }));

settingsSetup.ApplySettings(Settings);
settingsSetup.ApplySettings(newSettings);
}

var converters = settings.Converters
.Select(type => CreateConverter(type, system))
.ToList();

converters.Add(new SurrogateConverter(this));
converters.Add(new SurrogateConverter(surrogateParent));
converters.Add(new DiscriminatedUnionConverter());

foreach (var converter in converters)
{
Settings.Converters.Add(converter);
newSettings.Converters.Add(converter);
}

Settings.ObjectCreationHandling = ObjectCreationHandling.Replace; //important: if reuse, the serializer will overwrite properties in default references, e.g. Props.DefaultDeploy or Props.noArgs
Settings.ContractResolver = new AkkaContractResolver();

_serializer = JsonSerializer.Create(Settings);
newSettings.ObjectCreationHandling =
ObjectCreationHandling
.Replace; //important: if reuse, the serializer will overwrite properties in default references, e.g. Props.DefaultDeploy or Props.noArgs
newSettings.ContractResolver = new AkkaContractResolver();
return newSettings;
}



private static JsonConverter CreateConverter(Type converterType, ExtendedActorSystem actorSystem)
{
var ctor = converterType.GetConstructors()
Expand Down Expand Up @@ -228,12 +286,51 @@ 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))
using (var jw = new JsonTextWriter(tw))
{
_formattingNoneSerializer.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