C
C#13mo 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
Henkypenky13mo ago
$code
MODiX
MODiX13mo 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
Henkypenky13mo ago
there are ton of optimizations to be done dynamic, no
Tvde1
Tvde113mo ago
Is there any way you can not do a HTTP call every 100 ms?
Henkypenky
Henkypenky13mo ago
ReadAsString, no
Daltz333
Daltz33313mo ago
No, this is a real time application.
ffmpeg -i me -f null -
why dynamic and not jobject, at least
Daltz333
Daltz33313mo ago
But dynamic vs jobject does not address the root problem, which is GC pressure.
ffmpeg -i me -f null -
i know i know just a doubt
Henkypenky
Henkypenky13mo ago
don't use newtonsoft
Daltz333
Daltz33313mo ago
So I'd rather focus on solving that for now, I acknowledge that as a problem however.
Henkypenky
Henkypenky13mo ago
it hogs memory 8 times more
Tvde1
Tvde113mo ago
Are you in control of the other API? Able to convert to websockets or something other than JSON?
Henkypenky
Henkypenky13mo ago
than built in library
Daltz333
Daltz33313mo ago
Nope. Would take large amounts of investment effort.
Buddy
Buddy13mo ago
ffmpeg -i me -f null -
(like bson)
Buddy
Buddy13mo ago
If time and allocation is vital then you NEED to change to STJ
Daltz333
Daltz33313mo ago
I think I can incrementally switch to STJ in this specific case, but why does STJ decrease allocations that much?
Buddy
Buddy13mo ago
Takes use of Spans and is Source Generated afaik
Henkypenky
Henkypenky13mo ago
yeah almost everything was changed to spans
Tvde1
Tvde113mo ago
perhaps manual json parsing to a struct would be a lot of work but saves memory
Henkypenky
Henkypenky13mo ago
you can probably use a record struct it's faster
Tvde1
Tvde113mo ago
what's faster about a record struct?
Henkypenky
Henkypenky13mo ago
Daltz333
Daltz33313mo 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
Tvde113mo ago
can you share your bench code? because records are just classes
ffmpeg -i me -f null -
(what, class record is class, record is struct)
Tvde1
Tvde113mo ago
records are classes but record struct are structs if you omit struct, it's a class 😅
ffmpeg -i me -f null -
really? 🤔 then i'm mistaken
Henkypenky
Henkypenky13mo ago
record struct sure
Tvde1
Tvde113mo ago
that's pretty cool that it's faster, I would have assumed there would be no difference
Henkypenky
Henkypenky13mo 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
cap5lut13mo 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
Henkypenky13mo 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
cap5lut13mo 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
Daltz33313mo ago
Okay, switching to STJ yielded dramatic performance improvements Instead of spam GCs, it's one GC every ~3 seconds, totally within expectations
cap5lut
cap5lut13mo 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
Daltz33313mo ago
Already doing that 👍 for this specific case
cap5lut
cap5lut13mo ago
ah okay
ffmpeg -i me -f null -
have you tried also forcing gc?
cap5lut
cap5lut13mo 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
Henkypenky13mo 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
cap5lut13mo ago
I am writing a client application that consumes a web API
u got something mixed up ;p
Daltz333
Daltz33313mo 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.
ffmpeg -i me -f null -
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
cap5lut13mo 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
Accord13mo 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
More Posts
❔ .NET Service discoveryI have a gRPC server installed on a workstation in an on-premise local network. And I have client apHow do you get the output type from a powershell script with System.Management.Automation?I am trying to figure out how to get the type specified in a powershell script `[OutputType([int])]`❔ Make interface deriving from another interface have a sealed implementation of a function...so that you dont have to write the same code for every child interface. details are in pastebin: ❔ Serialize objects from non-standard formatHello, I am trying to serialize objects from a non-standard format and dont know how to go about it ❔ Rider accepting completion deletes the next wordWhen I hit tab in Rider to accept a suggestion, it overwrites everything that comes after. Just likeHow is my code?https://github.com/Jamboi2007/The-vote-game/blob/ccaf87812c4509c17b88e97dd639251c96252bc4/Home❔ c# access variable from other threadI've been searching the web for a while and I cant seem to access a variable like this: `img.Source❔ MAUI Error when loading resources on loading pageI have a view that displays the `Cards` property as a `CollectionView`. The view model is populating✅ Writing a scuffed way to use an "assignment" (=) operatorObviously, there's no such thing as an assignment operator. I'd like to find some way around that. I❔ Timing issues whole end-to-end testing microservices.I'm using MassTransit to communicate between microservies. This creates a problem when I'm writing t