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

Blazor Server Memory Usage #39238

Closed
gregoryagu opened this issue Dec 30, 2021 · 11 comments
Closed

Blazor Server Memory Usage #39238

gregoryagu opened this issue Dec 30, 2021 · 11 comments
Labels
area-blazor Includes: Blazor, Razor Components feature-blazor-server

Comments

@gregoryagu
Copy link

I have a Customer Service app which is used by about 10 staff. It is a .net 6 Blazor Server app. It uses EF Core 6 to retrieve and update data from Sql Server.

The app runs as expected as far as the end user is concerned.. However, what is unexpected is the memory usage on the server. The memory usage continually climbs until about 4 gigs of memory is used by the Application Pool at which point it is automatically recycled due to the settings on IIS for memory usage. In production, this recycling occurs about every two hours throughout the day. (Initially, this setting was not set, and it ate all the memory of the server until the server started throwing out of memory exceptions.)

The question is whether this is expected behavior or not. I have used the Diagnostic Tools in VS22 to see what is happening with the memory. The memory grows each time a page is accessed within the application. If the user stays on the same page, the memory stays level. But if that page is left, and then the user comes back to the page again, the memory raises, never to come back down again.

When the page is initialized, there are various lookup lists that are populated. For example, there is a list of Contacts with about 75 items. If I go to this page, and then take a memory snapshot, then I can see the count at 75 Contacts. If I then leave the page and go to a different page, and then come back again, the same list is again retrieved from the database, and in memory, there are now 150. The count grows every time the page is accessed.

It seems to me that the GC would be collecting these no longer used Contacts. But even if a force a GC with GC.Collect(), there is no reduction in the number of items.

So is this expected behavior? If not, what should I be doing to clean up these resources?

One thing I have found to help is that if I clear the Collection in the Dispose() method of the page, then these objects do NOT show up in the Heap, and after a GC, the memory is reclaimed.

For example, after going to the page 20 times without using List.Clear(), the heap size even after a GC is 41 Megs.

Using List.Clear() in the dispose() for 20 page views results in the heap size of 41, but after GC, it drops down to 23 Megs. So clearly this is beneficial. But as I understand garbage collection, the object which are no longer used in a page should be automatically cleaned up. Is this concept incorrect?

@TanayParikh TanayParikh added feature-blazor-server area-blazor Includes: Blazor, Razor Components labels Dec 30, 2021
@MariovanZeist
Copy link
Contributor

Hi @gregoryagu

You might be holding on to a DbContext too long, can you tell me how you instantiate the DbContext?
Are you injecting it into your component like [Inject] private MyDbContext Context { get; set; } or are you using a factory method where you get the DbContextuse it, and dispose of it when you are done?

There are some recommendations for using EF Core with Blazor server here: that might help you.

@gregoryagu
Copy link
Author

gregoryagu commented Dec 30, 2021

@MariovanZeist This is the code that creates the context:

For most data operations, this is the code that is used:

 using var context = this.DbFactory.CreateDbContext();
 var query = context.Market.OrderBy(st => st.Name);
 var items = await query.ToListAsync();
 this.Markets = new BindableCollection<Market>(items);

However, there is some data which is retrieved, tracked, and then saved to the database. For this:


         [Inject]
         public IDbContextFactory<CDSEntities> DbFactory { get; set; }

        private void InitializeDbContext()
        {
            if (this.DbContext != null)
            {
                this.DbContext.Dispose();
            }
            this.DbContext = this.DbFactory.CreateDbContext();
        }



This is what the dispose looks like:

    public void Dispose()
    {
        this.DbContext?.Dispose();
        this.StatusBarEvent.OnChange -= HandleStatusBarChangedEventAsync;
        this.GetType().GetProperties().Where(prop => prop.PropertyType.Name.StartsWith(nameof(BindableCollection<string>)) || prop.PropertyType.Name.StartsWith(nameof(ObservableCollection<string>)) || prop.PropertyType.Name.StartsWith(nameof(List<string>))).ToList().ForEach(prop => ((dynamic)prop.GetValue(this, null))?.Clear());
    }

So, as far as I know, i am coding this correctly per the guidance.

@MariovanZeist
Copy link
Contributor

MariovanZeist commented Jan 1, 2022

Hi @gregoryagu

I personally have deployed multiple applications that use Blazor Server and EF Core heavily without issues, And as you state that your application is only run concurrently by 10 users, my opinion is that you shouldn't run into memory problems.

My guess is that something is holding on to memory, as apparent by the need of that last line of code in the dispose:

this.GetType().GetProperties().Where(prop => prop.PropertyType.Name.StartsWith(nameof(BindableCollection<string>)) || prop.PropertyType.Name.StartsWith(nameof(ObservableCollection<string>)) || prop.PropertyType.Name.StartsWith(nameof(List<string>))).ToList().ForEach(prop => ((dynamic)prop.GetValue(this, null))?.Clear());

Although you said that you still had problems after adding the above line, it could be a symptom, but not the cause.

You could investigate the memory leak using this:. But as you already identified that just repeatedly calling a page will increase your memory I would focus on that.
If you run your code in the debugger, you could check your memory usage by creating a Memory Snapshot, reloading your page a couple of times, and then creating a new Memory Snapshot You can then compare both snapshots with each other, and see the differences.

But as I understand garbage collection, the object which are no longer used in a page should be automatically cleaned up. Is this concept incorrect?

You are correct, but it won't be garbage collected immediately, (And depending on some factors it might not even be cleared the next time the GC will collect the memory), It's memory will be reclaimed when the Garbage Collector deems it necessary, which depends on multiple factors that might even be fine-tuned during the running of your program, and it's not something you normally have to worry about.

@MariovanZeist
Copy link
Contributor

I noticed this in your code:

  this.StatusBarEvent.OnChange -= HandleStatusBarChangedEventAsync;

Make sure this is balanced (even amount of adds to remove) as this could potentially lead to memory leaks.

@gregoryagu
Copy link
Author

@MariovanZeist Thanks for the reply, I will follow the steps in the link you provided to see if I can determine exactly what the issue is. I agree with what you are saying in that clearing the collections should not be necessary. If I can come up with anything more specific, I will post back here. For now, I am going to close this issue.

@gregoryagu
Copy link
Author

So I am still struggling to determine exactly what the issue is. Here is what I have come up with so far:

  1. I open the app to the home page. I then go to a page called "Qual Kicks." I navigate to the index page, and back to this page 20 times.
  2. I then take a snapshot and view the heap. I filter the objects by "CDS.Pages" to show only the page classes. There are 20 instances of this class, which means that this class is not getting garbage collected. Meaning that something is holding a reference to the class.

image

  1. I then click on the class, and then click on the "Instances" button. It shows the 20 instances. I then click on one of them to try to find what is keeping it alive. This is what it shows me:
    image

But I don't get it. What is this telling me? There are a great number of items in the list. Are these all keeping the page in memory? What is it that is keeping the page in memory instead of allowing it to be Garbage Collected?

@MariovanZeist
Copy link
Contributor

@gregoryagu the last screenshot in your case isn't telling you much, it just tells you the memory that Page is holding on to, but you want the opposite, you want to know who holds on to this Page.
The second screenshot on the bottom half is telling you who is holding a reference to your Page, (So the Paths To Root tab) so it's the more interesting one.

What I would suggest is:
1.) Add a little debug button to maybe the status bar that calls GC.Collect()
2.) Create an almost empty page in your app. and go to that page
3.) Click on that GC.Collect button just before you are taking a snapshot.
4.) Flip between the empty page and your suspected page.
5.) End on the empty page again.
6.) press the GC.Collect button again, and create a second snapshot.
7.) Now compare the 2 snapshots, and see if your suspected page is still there, select it and check the Paths to Root tab

You might still have a lot of circular references in that list (A Page holding on to a table holding on to the Page again) these aren't necessarily your problem, as .NET can handle these circular references just fine.

But there will be a lot fewer objects to compare so it will be easier to spot the culprit.

There is a nice video you could check out here:

@gregoryagu
Copy link
Author

@MariovanZeist Thank you so much for your support. After quite a bit if digging, I have resolved the issue and it is no longer holding onto the pages and the memory leak is no longer occurring. So for now, I am going to let this rest. Again, thanks for providing guidance on resolving this.

@figueiredorj
Copy link

May I ask what was your issue?
thanks

@gregoryagu
Copy link
Author

gregoryagu commented Jan 25, 2022

It seemed to be a combination of things, include an event aggregator and using a base class for a page. The code really should not have leaked memory (from what I understand about GC and memory), but it did and through trial and error, we modified the code so that the memory leak no longer occurred. But there was no one single smoking gun. The lesson learned here for me is that memory usage monitoring should be a part of the development flow, not something tacked on at the end after the project is in beta. I wish I had a better explanation, but that's all I've got. :)

@figueiredorj
Copy link

figueiredorj commented Jan 25, 2022 via email

@ghost ghost locked as resolved and limited conversation to collaborators Feb 24, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components feature-blazor-server
Projects
None yet
Development

No branches or pull requests

4 participants