EF Core memory usage and QueryTrackingBehavior.NoTracking

Viewed 676

I have an ASP.NET Core 3 website that is frequently running out of memory on Azure.

enter image description here

One of the heavy-lifting (but frequently used) functions is to generate reports. So I thought I'd use one such report as a test case to see what's going on.

Here is a memory snapshot after the application loads, and then after 9 subsequent requests for one of the reports.

enter image description here

Looking at the diagnostics, lots of memory is consumed by EF change tracking objects.

enter image description here

I've found that if I use options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); in startup, then the snapshots for the same activity produces the following:

enter image description here

This is a massive improvement - adding 2 MB for every request is not viable. Is this normal - I would have thought that even with change tracking on, the GC wouldn't let it get this bad? Or could there be something in my report code that is making it hold onto references or something - I read that static variables in a class can lead to the GC not freeing up those instances, is that a possibility? I'm not sure if switching off some default functionality is just a band-aid to something else I'm doing fundamentally wrong (I'm pretty sure I'm disposing everyting with using statements, etc.).

1 Answers

I would say that such outcome is expected when switching all EF queries to be NoTracking, specially in reporting scenarios where you most likely are reading and then tracking tons of objects in memory.

In the official docs you can find detailed information about this topic. In there you can also see a benchmark comparing the performance of two queries, one that uses the change tracker and another one that doesn't, using a small data set (10 Blogs with 20 posts each). Despite the tiny amount of data, the results are similar to yours: almost a 40% increase in performance and the same-ish decrease in allocated memory.

Stats

Therefore, in regards to I'm not sure if switching off some default functionality is just a band-aid to something else I'm doing fundamentally wrong, I would definitely say that's not a band-aid solution at all to do it just for the reporting functionality. In these read-only scenarios where you need a performance boost, using non tracking queries is actually recommended.

However, the only thing I would be aware of is that probably you don't want to switch the tracking behaviour off for ALL queries in your application. By doing so, if you rely on the change tracker to perform updates of the entities somewhere else in the application, those updates will stop working.

For example:

var blog = context.Blogs
    .Where(blog => blog.Id = blogId)
    .SingleOrDeafult();

blog.Name = "Another Name";

context.SaveChanges() // If the default query behaviour is 'NoTracking', the Blog's name won't be updated since it wasn't in the ChangeTracker.

What I would do instead is to keep the default behaviour as tracking, but then I would change all queries that are used just in the reports to be done in a non tracking manner. To achieve this, you will have to add .AsNoTracking() in all the reporting EF queries.

For example:

var blogs = context.Blogs
    .AsNoTracking()
    .ToList();

This way, you will boost considerably the performance of your read-only queries, without affecting the rest of the application behaviour.

Related