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 Render Memory Growth #59970

Closed
1 task done
WhereRtheInterwebs opened this issue Jan 20, 2025 · 6 comments
Closed
1 task done

Blazor Render Memory Growth #59970

WhereRtheInterwebs opened this issue Jan 20, 2025 · 6 comments
Labels
area-blazor Includes: Blazor, Razor Components

Comments

@WhereRtheInterwebs
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Consider this Blazor Server code written in .NET 8. Why does calling StateHasChanged repeatedly cause the memory to climb into the hundreds of MB very quickly? Is there a better way to implement time sensitive, real-time updates? I can, of course, slow down the requests, but that only slows the memory growth rather than solves it.

@page "/"
@implements IDisposable
@using System.Threading

<PageTitle>Index</PageTitle>

<h1>@Value</h1>

@code{
    long Value = 0;
    CancellationTokenSource tokenSource = new();

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            _ = Task.Run(async () =>
            {
                while (!tokenSource.IsCancellationRequested)
                {
                    Value++;
                    await InvokeAsync(StateHasChanged);
                }
            }, tokenSource.Token);
        }
    }

    public void Dispose()
    {
        tokenSource.Cancel();
    }
}

Expected Behavior

I'm looking for a way to use Blazor Server to rapidly update simple values on the page without causing memory usage to explode.

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

8.0

Anything else?

dotnet --info
.NET SDK:
Version: 9.0.101
Commit: eedb237549
Workload version: 9.0.100-manifests.4a280210
MSBuild version: 17.12.12+1cce77968

Runtime Environment:
OS Name: Windows
OS Version: 10.0.22631
OS Platform: Windows
RID: win-x64
Base Path: C:\Program Files\dotnet\sdk\9.0.101\

.NET workloads installed:
[ios]
Installation Source: VS 17.12.35527.113
Manifest Version: 18.1.9163/9.0.100
Manifest Path: C:\Program Files\dotnet\sdk-manifests\9.0.100\microsoft.net.sdk.ios\18.1.9163\WorkloadManifest.json
Install Type: Msi

[android]
Installation Source: VS 17.12.35527.113
Manifest Version: 35.0.7/9.0.100
Manifest Path: C:\Program Files\dotnet\sdk-manifests\9.0.100\microsoft.net.sdk.android\35.0.7\WorkloadManifest.json
Install Type: Msi

[aspire]
Installation Source: VS 17.12.35527.113
Manifest Version: 8.2.2/8.0.100
Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.aspire\8.2.2\WorkloadManifest.json
Install Type: Msi

[wasm-tools]
Installation Source: VS 17.12.35527.113
Manifest Version: 9.0.0/9.0.100
Manifest Path: C:\Program Files\dotnet\sdk-manifests\9.0.100\microsoft.net.workload.mono.toolchain.current\9.0.0\WorkloadManifest.json
Install Type: Msi

[maui-windows]
Installation Source: VS 17.12.35527.113
Manifest Version: 9.0.0/9.0.100
Manifest Path: C:\Program Files\dotnet\sdk-manifests\9.0.100\microsoft.net.sdk.maui\9.0.0\WorkloadManifest.json
Install Type: Msi

[maccatalyst]
Installation Source: VS 17.12.35527.113
Manifest Version: 18.1.9163/9.0.100
Manifest Path: C:\Program Files\dotnet\sdk-manifests\9.0.100\microsoft.net.sdk.maccatalyst\18.1.9163\WorkloadManifest.json
Install Type: Msi

Configured to use loose manifests when installing new manifests.

Host:
Version: 9.0.0
Architecture: x64
Commit: 9d5a6a9aa4

.NET SDKs installed:
6.0.321 [C:\Program Files\dotnet\sdk]
9.0.101 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
Microsoft.AspNetCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 6.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 6.0.26 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 6.0.36 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 7.0.20 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 8.0.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 9.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.26 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.36 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 7.0.20 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 8.0.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 9.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 6.0.26 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 6.0.36 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 7.0.20 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 8.0.11 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 9.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
x86 [C:\Program Files (x86)\dotnet]
registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
Not set

global.json file:
Not found

Learn more:
https://aka.ms/dotnet/info

Download .NET:
https://aka.ms/dotnet/download

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label Jan 20, 2025
@willdean
Copy link

I do a lot of timed-update pages. This is my approach:

  • Create a system.threading.timer in the oninitialized handler.
  • In the timer callback, invoke StateHasChanged
  • Make sure all code in the callback is inside a try/catch block, you cannot let an exception escape the callback
  • Make the component IDisposable and dispose the Timer

But before worrying about memory leaks, make sure they’re actual, rooted, memory, not just garbage waiting for collection.

@WhereRtheInterwebs
Copy link
Author

With that technique do you see the memory rise? I manually called garbage collection and the memory was not reclaimed.

@willdean
Copy link

My experience is exclusively Blazor Server and is Linux and Windows on versions up to Net 8, and I don’t see leaks from that approach.

When I investigate leaks I always use a memory profiler of some kind to see what objects are actually leaked - I’m not sure what tools you have access to, but generally there are so many false reports of leaks on MS GH that you probably need a proper trace of what’s being leaked and how it’s rooted to get much traction around here.

@WhereRtheInterwebs
Copy link
Author

Modifying it to use a timer seems to have the same memory problem. New code:

@page "/test2"
@implements IDisposable
@using System.Threading

<PageTitle>Counter</PageTitle>

<h1>@Value</h1>

@code {
    long Value = 0;
    Timer timer;

    protected override void OnInitialized()
    {
        timer = new Timer(async _ =>
        {
            try
            {
                Value++;
                await InvokeAsync(StateHasChanged);
            }
            catch (Exception ex){
                Console.WriteLine(ex.ToString());
            }
        }, null, 0, 1);
    }

    public void Dispose()
    {
        timer.Change(Timeout.Infinite, Timeout.Infinite);
        timer.Dispose();
    }
}

@MariovanZeist
Copy link
Contributor

Hi @WhereRtheInterwebs
The issue you are seeing is most probably related to different Garbage collection strategies when running on a server/ or running on a workstation / debugging locally

On a server, the garbage collector will run less frequently (which is more performant as GC's cost time), resulting in more memory allocated (cause more memory isn't GC'd) so you probably will see a larger memory consumption.

But the GC will run when it's needed not when you think it should. My recommendation would be to not worry about it and let .NET handle the memory allocations / GC.

More in-depth information: https://learn.microsoft.com/en-us/aspnet/core/performance/memory?view=aspnetcore-9.0

@WhereRtheInterwebs
Copy link
Author

Hi @MariovanZeist,
Thank you for linking that article. It was extremely helpful. I didn't know about server vs workstation mode and server mode being default. I left the app running and it seems to have capped itself at 526 MB. I am running on a workstation and wanted to see the lower memory usage. Switching modes, it now holds the memory around 30-40 MB which is what I was expecting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

No branches or pull requests

3 participants