Skip to content

Commit a9859db

Browse files
Add StringBuilder pooling in NewtonsoftJsonSerializer (#4929)
* 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 <[email protected]>
1 parent 12c26c2 commit a9859db

File tree

5 files changed

+198
-12
lines changed

5 files changed

+198
-12
lines changed

src/benchmark/SerializationBenchmarks/Program.cs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,100 @@
66
//-----------------------------------------------------------------------
77

88
using System;
9+
using System.IO;
10+
using System.Linq;
911
using System.Runtime.CompilerServices;
12+
using System.Threading.Tasks;
1013
using Akka.Actor;
1114
using Akka.Configuration;
1215
using Akka.Serialization;
1316
using BenchmarkDotNet.Attributes;
1417
using BenchmarkDotNet.Running;
18+
using Newtonsoft.Json;
1519

1620
namespace SerializationBenchmarks
1721
{
1822
class Program
1923
{
2024
static void Main(string[] args)
2125
{
22-
BenchmarkRunner.Run<SerializationTests>();
26+
BenchmarkRunner.Run<JsonSerializerTests>();
27+
}
28+
}
29+
30+
public class TestSer
31+
{
32+
public int Id { get; set; }
33+
public string someStr { get; set; }
34+
public string someStr2 { get; set; }
35+
public string someStr3 { get; set; }
36+
public Guid IDK { get; set; }
37+
}
38+
[MemoryDiagnoser]
39+
public class JsonSerializerTests
40+
{
41+
public JsonSerializerTests()
42+
{
43+
_sys_noPool = ActorSystem.Create("bench-serialization-json-nopool",ConfigurationFactory.ParseString(@"
44+
akka.actor {{
45+
46+
serialization-settings {{
47+
json {{
48+
use-pooled-string-builder = false
49+
}}
50+
}}
51+
}}"));
52+
_sys_pool = ActorSystem.Create("bench-serialization-json-pool");
53+
_noPoolSer =
54+
_sys_noPool.Serialization.FindSerializerForType(typeof(object));
55+
_poolSer =
56+
_sys_pool.Serialization.FindSerializerForType(typeof(object));
57+
}
58+
private static TestSer testObj = new TestSer()
59+
{
60+
Id = 124,
61+
someStr =
62+
"412tgieoargj4a9349u2u-03jf3290rjf2390ja209fj1099u42n0f92qm93df3m-032jfq-102",
63+
someStr2 =
64+
"412tgieoargj4a9349u2u-03jf3290rjf2390ja209fj1099u42n0f92qm93df3m-032jfq-102",
65+
someStr3 =
66+
new string(Enumerable.Repeat('l',512).ToArray()),
67+
IDK = Guid.Empty
68+
};
69+
70+
private ActorSystem _sys_noPool;
71+
private ActorSystem _sys_pool;
72+
private Serializer _noPoolSer;
73+
private Serializer _poolSer;
74+
private const int _numIters = 10000;
75+
[Benchmark]
76+
public void Pooling()
77+
{
78+
for (int i = 0; i < _numIters; i++)
79+
{
80+
_poolSer.ToBinary(testObj);
81+
}
82+
}
83+
[Benchmark]
84+
public void NoPooling()
85+
{
86+
for (int i = 0; i < _numIters; i++)
87+
{
88+
_noPoolSer.ToBinary(testObj);
89+
}
90+
}
91+
92+
[Benchmark]
93+
public void Pooling_MultiTasks()
94+
{
95+
Task.WaitAll(Enumerable.Repeat(0, 10)
96+
.Select((l) => Task.Run(Pooling)).ToArray());
97+
}
98+
[Benchmark]
99+
public void NoPooling_MultiTasks()
100+
{
101+
Task.WaitAll(Enumerable.Repeat(0, 10)
102+
.Select((l) => Task.Run(NoPooling)).ToArray());
23103
}
24104
}
25105

src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4754,10 +4754,13 @@ namespace Akka.Serialization
47544754
public sealed class NewtonSoftJsonSerializerSettings
47554755
{
47564756
public static readonly Akka.Serialization.NewtonSoftJsonSerializerSettings Default;
4757-
public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjectReferences, System.Collections.Generic.IEnumerable<System.Type> converters) { }
4757+
public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjectReferences, System.Collections.Generic.IEnumerable<System.Type> converters, bool usePooledStringBuilder, int stringBuilderMinSize, int stringBuilderMaxSize) { }
47584758
public System.Collections.Generic.IEnumerable<System.Type> Converters { get; }
47594759
public bool EncodeTypeNames { get; }
47604760
public bool PreserveObjectReferences { get; }
4761+
public int StringBuilderMaxSize { get; }
4762+
public int StringBuilderMinSize { get; }
4763+
public bool UsePooledStringBuilder { get; }
47614764
public static Akka.Serialization.NewtonSoftJsonSerializerSettings Create(Akka.Configuration.Config config) { }
47624765
}
47634766
public sealed class NewtonSoftJsonSerializerSetup : Akka.Actor.Setup.Setup

src/core/Akka/Akka.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
</ItemGroup>
1616

1717
<ItemGroup>
18+
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.5" />
1819
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
1920
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
2021
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />

src/core/Akka/Configuration/Pigeon.conf

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -536,9 +536,29 @@ akka {
536536
}
537537

538538
# extra settings that can be custom to a serializer implementation
539-
serialization-settings {
540-
541-
}
539+
serialization-settings {
540+
json {
541+
542+
# Used to set whether to use stringbuilders from a pool
543+
# In memory constrained conditions (i.e. IOT)
544+
# You may wish to turn this off
545+
use-pooled-string-builder = true
546+
547+
# The starting size of stringbuilders created in pool
548+
# if use-pooled-string-builder is true.
549+
# You may wish to adjust this number,
550+
# For example if you are confident your messages are smaller or larger
551+
pooled-string-builder-minsize = 2048
552+
553+
# The maximum retained size of a pooled stringbuilder
554+
# if use-pooled-string-builder is true.
555+
# You may wish to turn this number up if your messages are larger
556+
# But do keep in mind that strings in .NET are UTF-16,
557+
# So after ~42k characters you might wind up
558+
# on the Large Object Heap (which may not be a bad thing...)
559+
pooled-string-builder-maxsize = 32768
560+
}
561+
}
542562
}
543563

544564
# Used to set the behavior of the scheduler.

src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
using System;
99
using System.Collections.Generic;
1010
using System.Globalization;
11+
using System.IO;
1112
using System.Linq;
1213
using System.Reflection;
1314
using System.Text;
1415
using Akka.Actor;
1516
using Akka.Configuration;
1617
using Akka.Util;
18+
using Microsoft.Extensions.ObjectPool;
1719
using Newtonsoft.Json;
1820
using Newtonsoft.Json.Converters;
1921
using Newtonsoft.Json.Linq;
@@ -32,7 +34,10 @@ public sealed class NewtonSoftJsonSerializerSettings
3234
public static readonly NewtonSoftJsonSerializerSettings Default = new NewtonSoftJsonSerializerSettings(
3335
encodeTypeNames: true,
3436
preserveObjectReferences: true,
35-
converters: Enumerable.Empty<Type>());
37+
converters: Enumerable.Empty<Type>(),
38+
usePooledStringBuilder:true,
39+
stringBuilderMinSize:2048,
40+
stringBuilderMaxSize:32768);
3641

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

5358
return new NewtonSoftJsonSerializerSettings(
5459
encodeTypeNames: config.GetBoolean("encode-type-names", true),
55-
preserveObjectReferences: config.GetBoolean("preserve-object-references", true),
56-
converters: GetConverterTypes(config));
60+
preserveObjectReferences: config.GetBoolean(
61+
"preserve-object-references", true),
62+
converters: GetConverterTypes(config),
63+
usePooledStringBuilder: config.GetBoolean("use-pooled-string-builder", true),
64+
stringBuilderMinSize:config.GetInt("pooled-string-builder-minsize", 2048),
65+
stringBuilderMaxSize:
66+
config.GetInt("pooled-string-builder-maxsize",
67+
32768)
68+
);
5769
}
5870

5971
private static IEnumerable<Type> GetConverterTypes(Config config)
@@ -89,21 +101,40 @@ private static IEnumerable<Type> GetConverterTypes(Config config)
89101
/// Converters must inherit from <see cref="JsonConverter"/> class and implement a default constructor.
90102
/// </summary>
91103
public IEnumerable<Type> Converters { get; }
104+
105+
/// <summary>
106+
/// The Starting size used for Pooled StringBuilders, if <see cref="UsePooledStringBuilder"/> is -true-
107+
/// </summary>
108+
public int StringBuilderMinSize { get; }
109+
/// <summary>
110+
/// The Max Retained size for Pooled StringBuilders, if <see cref="UsePooledStringBuilder"/> is -true-
111+
/// </summary>
112+
public int StringBuilderMaxSize { get; }
113+
/// <summary>
114+
/// If -true-, Stringbuilders are pooled and reused for serialization to lower memory pressure.
115+
/// </summary>
116+
public bool UsePooledStringBuilder { get; }
92117

93118
/// <summary>
94119
/// Creates a new instance of the <see cref="NewtonSoftJsonSerializerSettings"/>.
95120
/// </summary>
96121
/// <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>
97122
/// <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>
98123
/// <param name="converters">A list of types implementing a <see cref="JsonConverter"/> to support custom types serialization.</param>
99-
public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjectReferences, IEnumerable<Type> converters)
124+
/// <param name="usePooledStringBuilder">Determines if string builders will be used from a pool to lower memory usage</param>
125+
/// <param name="stringBuilderMinSize">Starting size used for pooled string builders if enabled</param>
126+
/// <param name="stringBuilderMaxSize">Max retained size used for pooled string builders if enabled</param>
127+
public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjectReferences, IEnumerable<Type> converters, bool usePooledStringBuilder, int stringBuilderMinSize, int stringBuilderMaxSize)
100128
{
101129
if (converters == null)
102130
throw new ArgumentNullException(nameof(converters), $"{nameof(NewtonSoftJsonSerializerSettings)} requires a sequence of converters.");
103131

104132
EncodeTypeNames = encodeTypeNames;
105133
PreserveObjectReferences = preserveObjectReferences;
106134
Converters = converters;
135+
UsePooledStringBuilder = usePooledStringBuilder;
136+
StringBuilderMinSize = stringBuilderMinSize;
137+
StringBuilderMaxSize = stringBuilderMaxSize;
107138
}
108139
}
109140

@@ -114,7 +145,8 @@ public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjec
114145
public class NewtonSoftJsonSerializer : Serializer
115146
{
116147
private readonly JsonSerializer _serializer;
117-
148+
149+
private readonly ObjectPool<StringBuilder> _sbPool;
118150
/// <summary>
119151
/// TBD
120152
/// </summary>
@@ -138,11 +170,15 @@ public NewtonSoftJsonSerializer(ExtendedActorSystem system, Config config)
138170
: this(system, NewtonSoftJsonSerializerSettings.Create(config))
139171
{
140172
}
141-
142-
173+
143174
public NewtonSoftJsonSerializer(ExtendedActorSystem system, NewtonSoftJsonSerializerSettings settings)
144175
: base(system)
145176
{
177+
if (settings.UsePooledStringBuilder)
178+
{
179+
_sbPool = new DefaultObjectPoolProvider()
180+
.CreateStringBuilderPool(settings.StringBuilderMinSize,settings.StringBuilderMaxSize);
181+
}
146182
Settings = new JsonSerializerSettings
147183
{
148184
PreserveReferencesHandling = settings.PreserveObjectReferences
@@ -183,6 +219,8 @@ public NewtonSoftJsonSerializer(ExtendedActorSystem system, NewtonSoftJsonSerial
183219
_serializer = JsonSerializer.Create(Settings);
184220
}
185221

222+
223+
186224
private static JsonConverter CreateConverter(Type converterType, ExtendedActorSystem actorSystem)
187225
{
188226
var ctor = converterType.GetConstructors()
@@ -228,12 +266,56 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ
228266
/// <param name="obj">The object to serialize </param>
229267
/// <returns>A byte array containing the serialized object</returns>
230268
public override byte[] ToBinary(object obj)
269+
{
270+
if (_sbPool != null)
271+
{
272+
return toBinary_PooledBuilder(obj);
273+
}
274+
else
275+
{
276+
return toBinary_NewBuilder(obj);
277+
}
278+
279+
}
280+
281+
private byte[] toBinary_NewBuilder(object obj)
231282
{
232283
string data = JsonConvert.SerializeObject(obj, Formatting.None, Settings);
233284
byte[] bytes = Encoding.UTF8.GetBytes(data);
234285
return bytes;
235286
}
236287

288+
private byte[] toBinary_PooledBuilder(object obj)
289+
{
290+
//Don't try to opt with
291+
//StringBuilder sb = _sbPool.Get()
292+
//Or removing null check
293+
//Both are necessary to avoid leaking on thread aborts etc
294+
StringBuilder sb = null;
295+
try
296+
{
297+
sb = _sbPool.Get();
298+
299+
using (var tw = new StringWriter(sb, CultureInfo.InvariantCulture))
300+
{
301+
var ser = JsonSerializer.CreateDefault(Settings);
302+
ser.Formatting = Formatting.None;
303+
using (var jw = new JsonTextWriter(tw))
304+
{
305+
ser.Serialize(jw, obj);
306+
}
307+
return Encoding.UTF8.GetBytes(tw.ToString());
308+
}
309+
}
310+
finally
311+
{
312+
if (sb != null)
313+
{
314+
_sbPool.Return(sb);
315+
}
316+
}
317+
}
318+
237319
/// <summary>
238320
/// Deserializes a byte array into an object of type <paramref name="type"/>.
239321
/// </summary>

0 commit comments

Comments
 (0)