C
C#2y ago
Daltz333

✅ Optimizing `HTTPClient.GetAsync()` and large strings

I am writing a client application that consumes a web API at a near real-time rate (100ms). I am using httpClient.GetAsync() to retrieve the content and then I grab the string contents with Content.ReadAsStringAsync(). Then, I deserialize it with JsonConvert.DeserializeObject<T>() to a format I expect (in some cases (out of my control), it's impossible to directly deserialize, in which case I deserialize to dynamic to parse manually). Because this is done every 100ms, it's creating a large number of GC (short, but frequent) creating some pretty bad GC pressure. This is on the count of ~10 ~5ms GCs/s. Unfortunately, this can dramatically slow down execution, especially since GCs block all threads, including GUI. I assume there are strategies to mitigate and solve this, this has to be a solved problem. Unfortunately, I'm not finding reliable/updated answers in google (the most common answer is... don't use C#).
49 Replies
Henkypenky
Henkypenky2y ago
$code
MODiX
MODiX2y ago
To post C# code type the following: ```cs // code here ``` Get an example by typing $codegif in chat If your code is too long, post it to: https://paste.mod.gg/
Henkypenky
Henkypenky2y ago
there are ton of optimizations to be done dynamic, no
Tvde1
Tvde12y ago
Is there any way you can not do a HTTP call every 100 ms?
Henkypenky
Henkypenky2y ago
ReadAsString, no
Daltz333
Daltz333OP2y ago
No, this is a real time application.
Omnissiah
Omnissiah2y ago
why dynamic and not jobject, at least
Daltz333
Daltz333OP2y ago
But dynamic vs jobject does not address the root problem, which is GC pressure.
Omnissiah
Omnissiah2y ago
i know i know just a doubt
Henkypenky
Henkypenky2y ago
don't use newtonsoft
Daltz333
Daltz333OP2y ago
So I'd rather focus on solving that for now, I acknowledge that as a problem however.
Henkypenky
Henkypenky2y ago
it hogs memory 8 times more
Tvde1
Tvde12y ago
Are you in control of the other API? Able to convert to websockets or something other than JSON?
Henkypenky
Henkypenky2y ago
than built in library
Daltz333
Daltz333OP2y ago
Nope. Would take large amounts of investment effort.
Buddy
Buddy2y ago
Omnissiah
Omnissiah2y ago
(like bson)
Buddy
Buddy2y ago
If time and allocation is vital then you NEED to change to STJ
Daltz333
Daltz333OP2y ago
I think I can incrementally switch to STJ in this specific case, but why does STJ decrease allocations that much?
Buddy
Buddy2y ago
Takes use of Spans and is Source Generated afaik
Henkypenky
Henkypenky2y ago
yeah almost everything was changed to spans
Tvde1
Tvde12y ago
perhaps manual json parsing to a struct would be a lot of work but saves memory
Henkypenky
Henkypenky2y ago
you can probably use a record struct it's faster
Tvde1
Tvde12y ago
what's faster about a record struct?
Henkypenky
Henkypenky2y ago
Daltz333
Daltz333OP2y ago
I can't deserialize directly into structs, I have to at the very least manually parse the string due to the bad formatting on the API (dynamically change key names:()
Tvde1
Tvde12y ago
can you share your bench code? because records are just classes
Omnissiah
Omnissiah2y ago
(what, class record is class, record is struct)
Tvde1
Tvde12y ago
records are classes but record struct are structs if you omit struct, it's a class 😅
Omnissiah
Omnissiah2y ago
really? 🤔 then i'm mistaken
Henkypenky
Henkypenky2y ago
record struct sure
Tvde1
Tvde12y ago
that's pretty cool that it's faster, I would have assumed there would be no difference
Henkypenky
Henkypenky2y ago
put it in release and just run it it's based on mike's benchmark i always take them to build mine's lol shameless you'll also have to remove the constructor from the record struct i did some modification i can't remember it was several months ago and the properties
cap5lut
cap5lut2y ago
depending on how dynamic it is, u could use a custom converter instead. https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-7-0 and if u can have it as converter, u can directly pass the stream, tho that wont help that much with gc pressure.
Henkypenky
Henkypenky2y ago
public record struct UsingRecord(int Phone, int Age);
public record struct UsingRecord(int Phone, int Age);
| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|------------------------ |------ |-----------:|-----------:|-----------:|------:|--------:|--------:|----------:|------------:|
| SerializingUsingClasses | 10 | 5.254 us | 0.1011 us | 0.1349 us | 1.19 | 0.05 | 0.6638 | 2.73 KB | 2.50 |
| SerializingUsingRecords | 10 | 4.366 us | 0.0866 us | 0.1584 us | 1.00 | 0.00 | 0.2670 | 1.09 KB | 1.00 |
| | | | | | | | | | |
| SerializingUsingClasses | 100 | 56.011 us | 1.0992 us | 2.1178 us | 1.26 | 0.05 | 6.6528 | 27.34 KB | 2.50 |
| SerializingUsingRecords | 100 | 44.125 us | 0.6781 us | 0.6659 us | 1.00 | 0.00 | 2.6245 | 10.94 KB | 1.00 |
| | | | | | | | | | |
| SerializingUsingClasses | 1000 | 560.854 us | 11.1538 us | 20.6742 us | 1.19 | 0.05 | 68.3594 | 280.47 KB | 2.41 |
| SerializingUsingRecords | 1000 | 472.964 us | 9.1964 us | 12.8921 us | 1.00 | 0.00 | 28.3203 | 116.41 KB | 1.00 |
| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|------------------------ |------ |-----------:|-----------:|-----------:|------:|--------:|--------:|----------:|------------:|
| SerializingUsingClasses | 10 | 5.254 us | 0.1011 us | 0.1349 us | 1.19 | 0.05 | 0.6638 | 2.73 KB | 2.50 |
| SerializingUsingRecords | 10 | 4.366 us | 0.0866 us | 0.1584 us | 1.00 | 0.00 | 0.2670 | 1.09 KB | 1.00 |
| | | | | | | | | | |
| SerializingUsingClasses | 100 | 56.011 us | 1.0992 us | 2.1178 us | 1.26 | 0.05 | 6.6528 | 27.34 KB | 2.50 |
| SerializingUsingRecords | 100 | 44.125 us | 0.6781 us | 0.6659 us | 1.00 | 0.00 | 2.6245 | 10.94 KB | 1.00 |
| | | | | | | | | | |
| SerializingUsingClasses | 1000 | 560.854 us | 11.1538 us | 20.6742 us | 1.19 | 0.05 | 68.3594 | 280.47 KB | 2.41 |
| SerializingUsingRecords | 1000 | 472.964 us | 9.1964 us | 12.8921 us | 1.00 | 0.00 | 28.3203 | 116.41 KB | 1.00 |
about 20% faster, 1/3 alloc ~
cap5lut
cap5lut2y ago
or maybe instead that and reading from stream, still read it to a string and just try to determine the property names and use a custom naming policy: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/customize-properties?pivots=dotnet-7-0#use-a-custom-json-property-naming-policy
Daltz333
Daltz333OP2y ago
Okay, switching to STJ yielded dramatic performance improvements Instead of spam GCs, it's one GC every ~3 seconds, totally within expectations
cap5lut
cap5lut2y ago
with the other hints u might be able to reduce it even more 😄 and once more regarding dynamic: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/use-dom#use-jsondocument-for-access-to-data this will probably still be better in case the custom converter/naming policy wouldnt be enough
Daltz333
Daltz333OP2y ago
Already doing that 👍 for this specific case
cap5lut
cap5lut2y ago
ah okay
Omnissiah
Omnissiah2y ago
have you tried also forcing gc?
cap5lut
cap5lut2y ago
the issue was that the gc kicked in too often because too much garbage was produced. so forcing the gc would just worsen it
Henkypenky
Henkypenky2y ago
just switching to STJ is a good step on the right direction, unfortunately newtonsoft produces 8 times more allocations, that's a huge memory footprint another thing you can do is switch from just using HttpClient to the Interface IHttpClientFactory, which will allow you to have them automatically created and disposed for you. I recommend Typed Client mode https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-7.0 also, one more thing, depending on the distance between the endpoints you will never be able to get that number down significicantly just because of the time it takes for the data to travel from one location to another. So this is a big consideration but you can work on your server to drastically drop down the processing time and the likes
cap5lut
cap5lut2y ago
I am writing a client application that consumes a web API
u got something mixed up ;p
Daltz333
Daltz333OP2y ago
Yep yep. It's satisfactory now. Now if only Livecharts2 didn't perform 6 allocations for every plot point ha but that's a separate problem lol.
Omnissiah
Omnissiah2y ago
if you call gc when you know you have finished using something maybe it could be easier on the gc automation, i mean is it not worth trying? there's also GC.SuppressFinalize()
cap5lut
cap5lut2y ago
the point was to reduce the amount that actually has to be garbage collected, not about when to collect it (and forcing the gc to do so is in most cases bad anyway - tests excluded ofc) GC.SuppressFinalize() doesnt apply here either, there are no unmanaged resources involved that would require implementing a finalizer nor to even suppress it. 2 other things about finalizers: 1) there is no guarantee that there are called in every case 2) if not written carefully they can even bring the object back to live just to let the gc try to collect it in the next iteration again. they are almost always only used as last resort to release some unmanaged resources (eg if u forget to dispose a texture u have uploaded to the graphics card's memory this is a last try to get off said memory, else it probably gonna reside in there til computer shutdown)
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server