benruehl / adonis-ui

Lightweight UI toolkit for WPF applications offering classic but enhanced windows visuals
https://benruehl.github.io/adonis-ui/
MIT License
1.71k stars 143 forks source link

High CPU Usage with Circle Loading Indicator #93

Closed alexhelms closed 4 years ago

alexhelms commented 4 years ago

Describe the bug I noticed high CPU usage when using the circle loading indicator. My control visibility is changed with a binding. Even when the control is collapsed or hidden, WPF appears to be still animating and causing high CPU usage.

To Reproduce Here is a snippet of xaml, when I put this in my control, I can observe high CPU.

<ContentControl Width="24"
        Height="24"
        Margin="{adonisUi:Space 1}"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        ContentTemplate="{DynamicResource {x:Static adonisUi:Templates.LoadingCircle}}"
        Focusable="False"
        Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
        Visibility="{TemplateBinding IsConnecting, Converter={x:Static converters:Converters.VisibleIfTrue}}" />

Expected behavior CPU shouldn't be high when a control is collapsed.

Screenshots image

Screenshot above is from Process Explorer when my application is sitting idle. There are 5 controls that have the xaml snipet from above. The indicator is collapsed.

Additional context I believe this may be a manifestation of this WPF bug: https://github.com/dotnet/wpf/issues/1509 But I'm not sure. I need to do some more experimentation.

benruehl commented 4 years ago

I looked into this but I don't think this can be handled by Adonis UI. WPF is responsible for rendering the UI. If WPF decides that collapsed controls still need to be processed then that's the way. At stackoverflow they talk about a very little performance impact though. https://stackoverflow.com/a/18553873

Is this a real issue for you that you can "feel" when using your app or is it only measurable but not visible? Is it only an issue for that specific animation or are others causing it as well?

What you could do is create a trigger on Visibility == Visibility.Collapsed that sets ContentTemplate to {x:Null}. That way the animation is removed from the visual tree completely instead of just hiding it. The disadvantage here is that when it becomes visible again the resource has to be looked up again which might be a small performance impact as well. But you could try if this works better for your scenario.

Something like this:

<ContentControl Width="24"
                Height="24"
                Margin="{adonisUi:Space 1}"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Focusable="False"
                Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
                Visibility="{TemplateBinding IsConnecting, Converter={x:Static converters:Converters.VisibleIfTrue}}" >
    <ContentControl.Style>
        <Style TargetType="ContentControl">
            <Setter Property="ContentTemplate" Value="{DynamicResource {x:Static adonisUi:Templates.LoadingCircle}}"/>
            <Style.Triggers>
                <Trigger Property="Visibility" Value="Collapsed">
                    <Setter Property="ContentTemplate" Value="{x:Null}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </ContentControl.Style>
</ContentControl>
alexhelms commented 4 years ago

This library has a similar loading indicator but does not have the render thread issue. I've been using that as a work around for now. I think it may come down to the fact that the Adonis animations don't have an end time. The animations are always "on" so that combined with the WPF bug makes the renderer always want to render the animation even though the control is collapsed.

I discovered this on low end hardware running my application. There were maybe 10 loading circles that were collapsed and WPF was taking up a significant portion of the CPU when the application was idle. It was slowing down all animations and making UI interaction slower than it should be.

That said, I tried your work around to set the ContentTemplate to null and I can confirm that does work -- I no longer see high CPU usage from WPF.

benruehl commented 4 years ago

I looked at the source code of LoadingIndicators.WPF as you suggested and even debugged it on my machine but I could not find any reason why they don't have this problem. There is nothing they explicitly do to prevent the animations from running while the controls are invisible. They use a different approach of creating the loading indicators though. While in Adonis UI the loading indicators are DataTemplates applied as ContentTemplate, in LoadingIndicators.WPF they have a custom LoadingIndicator control with different styles and control templates for each animation. So maybe WPF handles control templates and content templates differently.

Anyway, I'm glad you found some workaround.

Considering the options I have I decided to not fix this for now. The only solution I see for this is rebuilding the loading indicators the way they are built in LoadingIndicators.WPF and hoping that this will fix it. But I don't want to recreate an existing working library.

There is still a chance this gets fixed in WPF with the .NET 5.0 update as described in dotnet/wpf#1509 as well.

If you want to optimize performance for slow machines in general, you could try the approach described here. This limits the framerate of all animations in you application to a desired value.

I hope this decision is okay for you. In case others experience this issue as well, I might reconsider my choice.

Thank you very much for reporting.

alexhelms commented 4 years ago

Thank you for taking a look. The work around you suggested is working great and I'm fine with using that. Lets hope this bug gets fixed in WPF, it seems like anything we do is just working around the true problem in WPF.