✅ 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
$code
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/there are ton of optimizations to be done
dynamic, no
Is there any way you can not do a HTTP call every 100 ms?
ReadAsString, no
No, this is a real time application.
why dynamic and not jobject, at least
But dynamic vs jobject does not address the root problem, which is GC pressure.
i know i know
just a doubt
don't use newtonsoft
So I'd rather focus on solving that for now, I acknowledge that as a problem however.
it hogs memory
8 times more
Are you in control of the other API? Able to convert to websockets or something other than JSON?
than built in library
Nope. Would take large amounts of investment effort.
(like bson)
If time and allocation is vital then you NEED to change to STJ
I think I can incrementally switch to STJ in this specific case, but why does STJ decrease allocations that much?
Takes use of Spans and is Source Generated afaik
yeah almost everything was changed to spans
perhaps manual json parsing to a struct would be a lot of work but saves memory
you can probably use a record struct
it's faster
what's faster about a record struct?
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:()
can you share your bench code?
because records are just classes
(what, class record is class, record is struct)
records are classes but record struct are structs
if you omit struct, it's a class 😅
really? 🤔 then i'm mistaken
record struct
sure
that's pretty cool that it's faster, I would have assumed there would be no difference
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
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.
about 20% faster, 1/3 alloc ~
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
Okay, switching to STJ yielded dramatic performance improvements
Instead of spam GCs, it's one GC every ~3 seconds, totally within expectations
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 enoughAlready doing that 👍 for this specific case
ah okay
have you tried also forcing gc?
the issue was that the gc kicked in too often because too much garbage was produced.
so forcing the gc would just worsen it
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
I am writing a client application that consumes a web APIu got something mixed up ;p
Yep yep. It's satisfactory now. Now if only Livecharts2 didn't perform 6 allocations for every plot point but that's a separate problem lol.
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()
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)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.