1

In a blazor server application, is it okay to send events and call StateHasChanged very often, e.g., 500 times per second?

One of my pages needs to react to an external event and then update its state accordingly. I found the following solution:

  • Create a service that detects the external event and invokes a C# event.
  • Inject the service into the razor page.
  • In the page, connect to the event and call InvokeAsync(() => StateHasChanged()) in the handler.

This already works correctly. However, the event may occur very often, e.g., 500 times per second, and I worry about the performance of client and server. Unfortunately, I dont understand which part happens on the server, which part happens on the client, and which data is sent between them.

  • Are the events actually sent 500 times per second from the server to the client? I think this would consume a lot of bandwidth.
  • Does the client actually render the page after each call to StateHasChanged? I think this would impose a high CPU load on the client.
pschill
  • 5,055
  • 1
  • 21
  • 42
  • 3
    Do you also have a User who can look 500 times per second? – H H Mar 02 '21 at 11:16
  • @HenkHolterman No I dont :) I dont really want to update the GUI that frequently. However, it is easy to just connect to the event and call StateHasChanged. It is (a little bit) more work to collect multiple events and handle them all together. So if blazor already performs some buffering of StateHasChanged, I dont need to reimplement my own strategy. – pschill Mar 02 '21 at 12:07

2 Answers2

3

To answer some of your questions, as your running in Server mode all the real work takes place in the Blazor Hub session.

What a call to StateHasChanged really does is queue a RenderFragment onto the Renderer Queue in the Hub Session. Here's the bit of code from ComponentBase.

           _renderFragment = builder =>
            {
                _hasPendingQueuedRender = false;
                _hasNeverRendered = false;
                BuildRenderTree(builder);
            };

StateHasChanged looks like this:

       protected void StateHasChanged()
        {
            if (_hasPendingQueuedRender) return;
            if (_hasNeverRendered || ShouldRender())
            {
                _hasPendingQueuedRender = true;
                try
                {
                    _renderHandle.Render(_renderFragment);
                }
                catch
                {
                    _hasPendingQueuedRender = false;
                    throw;
                }
            }
        }

StateHasChanged only queues a new render event if one isn't already queued. Once rendered, the Renderer diffing engine detects any changes and sends just those changes to the Client Browser Session over SignalR.

So no changes, no client activity, just lots of server bound activity dealing with the events and working out any changes. The impact on the server will depend upon how much server power you have available.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • The _hasPendingQueuedRender logic is the greatest reassusrance here. It would still be interesting to see how often a render is started. – H H Mar 02 '21 at 14:15
  • Not sure about this answer. There is a queue, but also, there is a way to flush the queue (at the end of a task for example). What happens if you flush the queue 500 times per second? The queue is to store changes within a non rendered block (ex: inside a task) – dani herrera Mar 02 '21 at 14:56
  • @daniherrera - the queue here has always Length <= 1. See the first line of StateHasChanged(). – H H Mar 02 '21 at 16:39
  • @HenkHolterman, you know `StateHasChanged` doesn't force a render just flagged the component to be rendered. When I have some time I will dig into source code to read the whole class. – dani herrera Mar 02 '21 at 17:02
  • @daniherrera. Hi, can you explain what you mean by there is a way to flush the queue (at the end of a task for example)? – MrC aka Shaun Curtis Mar 02 '21 at 17:54
  • @ShaunCurtis Look this Q&A about how to force render (start spinning) in the half of a task https://stackoverflow.com/a/56675814/842935 – dani herrera Mar 02 '21 at 18:02
  • @daniherrera . Thanks, will read. Are we talking at cross purposes? The Render Queue is a component of the Renderer, not directly associated with the Task Scheduler. `await Task.Delay(1)` works in what you've referenced because it yields thread control back to the Task Scheduler, which then runs the UI code. – MrC aka Shaun Curtis Mar 02 '21 at 18:32
  • . There is a proviso to quote MS documentation - "However, the context will decide how to prioritize this work relative to other work that may be pending. The synchronization context that is present on a UI thread in most UI environments will often prioritize work posted to the context higher than input and rendering work." – MrC aka Shaun Curtis Mar 02 '21 at 18:32
3

It looks that Blazor server can send hundreds of changes by second:

enter image description here

@page "/"
Tics per second: <input type="range" min="1" max="2000" @bind="@CurrentValue" class="slider" id="myRange"> @CurrentValue
<div style="width:500px; height:10px; background-color: blue; position: relative;">
    <div class="ball" style="@position_txt"></div>
</div> <br/><br/>
<span>@DateTime.Now.ToString("HH:mm:ss")</span>
<span>Number of renders: @nRenders.ToString("N0")</span>
<button type="button" @onclick="start">start</button>
<style>
 .ball {width: 30px; height: 30px; top: -10px;
        position: absolute; background-color: blue;}
</style>
@code
{
    Int64 nRenders = 0, v = 1, position = 10, CurrentValue = 10;
    string position_txt => $"left: {position}px;";
    private static System.Timers.Timer aTimer = new System.Timers.Timer();
    protected void start()
    {
        move();
        aTimer.Elapsed += (source, e) => move();
        aTimer.AutoReset = true;
        aTimer.Enabled = !aTimer.Enabled;
    }
    protected void move()
    {
        aTimer.Interval = 1000.0/CurrentValue;
        position = (position+v);
        if (position>500 || position<0) v *= -1;
        InvokeAsync(StateHasChanged);
    }
    protected override void OnAfterRender(bool firstRender) => nRenders++;
}
dani herrera
  • 48,760
  • 8
  • 117
  • 177
  • Changes: yes. But when you get the interval beween OnAfterRenders you will see it's 12-15 ms, independent of the ticks/second. – H H Mar 02 '21 at 16:58
  • @HenkHolterman, In some point is aggregating or discarting diffs? – dani herrera Mar 02 '21 at 17:05
  • 2
    It aggregates the html diffs. Like you said, StateHasChanged() doesn't force a render. When a render is already pending, it does nothing. – H H Mar 02 '21 at 17:13
  • I have the feeling that this is not related with render and state has changed. I feel it is related with transport (signalR). 15ms is like 60fps. Enough for a good UX. But just a feeling. – dani herrera Mar 02 '21 at 18:06
  • Late to the party, but this is the thing that I noticed, specifically with a range input. The network traffic doesn't concern me, but you will spike the CPU on the server with this kind of action, which is not ideal if you have more than a few users do it at the same time. – Jeff Putz Jul 07 '23 at 02:31