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

Conversation

to11mtm
Copy link
Member

@to11mtm to11mtm commented Apr 10, 2021

This PR Gives us a boost on NewtonsoftJsonSerializer's Serialization Throughput.

By lowering memory pressure, we see a ~5% gain under a single thread, and >25% gain under a little bit of Task pressure:

Edit- after Fixes, Some tricky thread things going on in JsonSerializer...:

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
Pooling 42.30 ms 0.343 ms 0.304 ms 11000.0000 - - 49.67 MB
NoPooling 44.76 ms 0.310 ms 0.259 ms 15833.3333 83.3333 - 71.26 MB
Pooling_MultiTasks 126.83 ms 1.899 ms 1.683 ms 115500.0000 3750.0000 - 496.71 MB
NoPooling_MultiTasks 176.41 ms 3.517 ms 3.763 ms 173000.0000 15000.0000 - 712.63 MB

@to11mtm to11mtm force-pushed the newtonsoftjsonserializer-pooled-stringbuilder branch from 008c0cc to 7c72de5 Compare April 10, 2021 16:51
Comment on lines 526 to 545
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.

Comment on lines 189 to 193
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.

Comment on lines 196 to 197
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.)

@Aaronontheweb
Copy link
Member

Looks very promising @to11mtm

@to11mtm
Copy link
Member Author

to11mtm commented Apr 10, 2021

Well, this tripped a bunch of specs, but different depending on environment... I'm guessing the pooling initialization is in some cases causing some of these specs to time out.

@Aaronontheweb
Copy link
Member

@to11mtm no, these look like some of the usual racy specs to me

@Aaronontheweb
Copy link
Member

Ready to give this a real review when you say so @to11mtm

@to11mtm to11mtm marked this pull request as ready for review September 15, 2021 16:54
@to11mtm
Copy link
Member Author

to11mtm commented Sep 15, 2021

Ready to give this a real review when you say so @to11mtm

I think this is GTG, although I wonder if we can/should change the max-pool-size.

The challenge is that payloads over 32k will wind up invalidating the cache, but larger sizes can wind up on the LOH because of the way StringBuilder.Clear() works.

This really isn't a bad bad thing TBH, but worth deciding whether we want to do that.

@thzinc
Copy link

thzinc commented Nov 10, 2021

I came across this while researching serialization issues of my own and arriving at NewtonSoftJsonSerializer. I noticed that the ToBinary and FromBinary implementations here could probably benefit from avoiding string altogether by using streams. (Ref from performance tips docs)

I'm testing out the following changes:

        public override byte[] ToBinary(object obj)
        {
            using (var stream = new MemoryStream())
            using (var writer = new StreamWriter(stream))
            {
                _serializer.Serialize(writer, obj);
                return stream.ToArray();
            }
        }
        // snip
        public override object FromBinary(byte[] bytes, Type type)
        {
            using (var stream = new MemoryStream(bytes))
            using (var r = new StreamReader(stream))
            using (var reader = new JsonTextReader(r))
            {
                object res = _serializer.Deserialize(reader);
                return TranslateSurrogate(res, this, type);
            }
        }

I was planning to submit a PR with this change, but I see that obviously it would clash with this work. Is there some other benefit to using this StringBuilder implementation over Json.NET's stream operations?

@Aaronontheweb
Copy link
Member

I came across this while researching serialization issues of my own and arriving at NewtonSoftJsonSerializer. I noticed that the ToBinary and FromBinary implementations here could probably benefit from avoiding string altogether by using streams. (Ref from performance tips docs)

I'm testing out the following changes:

        public override byte[] ToBinary(object obj)
        {
            using (var stream = new MemoryStream())
            using (var writer = new StreamWriter(stream))
            {
                _serializer.Serialize(writer, obj);
                return stream.ToArray();
            }
        }
        // snip
        public override object FromBinary(byte[] bytes, Type type)
        {
            using (var stream = new MemoryStream(bytes))
            using (var r = new StreamReader(stream))
            using (var reader = new JsonTextReader(r))
            {
                object res = _serializer.Deserialize(reader);
                return TranslateSurrogate(res, this, type);
            }
        }

I was planning to submit a PR with this change, but I see that obviously it would clash with this work. Is there some other benefit to using this StringBuilder implementation over Json.NET's stream operations?

IMHO, send this PR in anyway so we can benchmark it. Would love to see what kind of difference this makes. I don't think neither @to11mtm nor myself have a strong opinion about what approach we go with - we just care that the approach is compatible with the wire / API and is faster than what we have today.

@thzinc
Copy link

thzinc commented Nov 10, 2021

@Aaronontheweb submitted #5376

@Aaronontheweb
Copy link
Member

... I honestly have no idea why we haven't merged this already though. Must have been me getting distracted with other stuff.

@Aaronontheweb
Copy link
Member

Excellent, thank you @thzinc - I'm about to publish Akka.NET v1.4.28 but I will take a look at your PR!

@to11mtm
Copy link
Member Author

to11mtm commented Nov 11, 2021

... I honestly have no idea why we haven't merged this already though. Must have been me getting distracted with other stuff.

Or you want me to go away. That said, Merge this and I'll do the Threadpool deed. :)

Copy link
Member

@Aaronontheweb Aaronontheweb left a comment

Choose a reason for hiding this comment

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

LGTM

@@ -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

{
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

@Aaronontheweb Aaronontheweb added this to the 1.4.29 milestone Nov 22, 2021
@Aaronontheweb Aaronontheweb enabled auto-merge (squash) November 22, 2021 19:07
@Aaronontheweb Aaronontheweb modified the milestones: 1.4.29, 1.4.30 Dec 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants