-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Changes from 2 commits
0d5b076
7c72de5
66c87f4
379a51f
ea00658
2727bfa
9edbbf6
edbff06
e0a83de
b343de8
e07aa41
3677850
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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"/>. | ||
|
@@ -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) | ||
|
@@ -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; | ||
} | ||
} | ||
|
||
|
@@ -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> | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
{ | ||
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() | ||
|
@@ -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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
There was a problem hiding this comment.
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