Skip to content

CompositionTarget.Rendering event slows down when there are no UI updates #1908

Open
@dotMorten

Description

@dotMorten
  • .NET Core Version:3.0.100-rc1-014190
  • Windows version: 1903
  • Does the bug reproduce also in WPF for .NET Framework 4.8?: Yes
  • Is this bug related specifically to tooling in Visual Studio (e.g. XAML Designer, Code editing, etc...)? No

Problem description:
When relying on CompositionTarget.Rendering, I've noticed that the event throttles down to around 50fps when there are no UI updates. As soon as UI updates are occuring, the event goes back up to firing at 60fps.

This becomes problematic when you're doing performance metrics and want to ensure performance stays a consistent 60fps, but when things stops moving, it shows in the performance numbers as really bad performance.

Notice here where no UI updates are done, the framerate is around 50fps. Once animation is started, the framerate goes up to 60:
Untitled Project

Actual behavior:
Clock timer slows down.

Expected behavior:
Event sticks to 60fps

Minimal repro:
Use the toggle button to turn the animation on and off. Monitor the output window for framerate updates every 0.5 seconds.

MainWindow.xaml:

<Window x:Class="FrameRateTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FrameRateTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
        <Window.Resources>
            <Storyboard x:Key="sb" RepeatBehavior="Forever">
               <DoubleAnimation Storyboard.TargetName="tt" 
                       Storyboard.TargetProperty="X" From="-250" To="250" 
                       Duration="0:0:5" BeginTime="0:0:0"/>
               <DoubleAnimation Storyboard.TargetName="tt" 
                       Storyboard.TargetProperty="X" From="250" To="-250" 
                       Duration="0:0:5" BeginTime="0:0:5"/>
            </Storyboard>
        </Window.Resources>

    <Grid>
        
        <Button HorizontalAlignment="Left" VerticalAlignment="Top" Content="Toggle Animation" Click="ToggleAnimation_Click" />
        
        <Ellipse Width="30" Height="30" Fill="Red" HorizontalAlignment="Center" VerticalAlignment="Center">
           <Ellipse.RenderTransform>
              <TranslateTransform x:Name="tt" />
           </Ellipse.RenderTransform>
        </Ellipse>
    </Grid>
    
</Window>

MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace FrameRateTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            CompositionTarget.Rendering += OnRender;
            stopwatch.Start();
        }

        Stopwatch stopwatch = new Stopwatch();
        TimeSpan lastFrame = TimeSpan.Zero;
        MovingAverage avr = new MovingAverage(60);
        int frame = 0;
        public void OnRender(object sender, EventArgs e)
        {
            frame++;
            var ts = stopwatch.Elapsed;
            avr.AddSample((ts - lastFrame).TotalMilliseconds);
            lastFrame = ts;
            if (frame % 30 == 0)
            {
                Debug.WriteLine((1000 / avr.Average).ToString("0.000"));
            }
        }
        bool isAnimating;
        public void ToggleAnimation_Click(object sender, RoutedEventArgs e)
        {
            var sb = (Storyboard)Resources["sb"];
            if (isAnimating)
                sb.Begin();
            else
                sb.Stop();
            isAnimating = !isAnimating;

        }
    }

    public class MovingAverage
    {
        private Queue<double> _samples;
        private int _windowSize;
        private double _sum;

        public double Average
        {
            get
            {
                int cnt = _samples.Count;
                return cnt != 0 ? _sum / cnt : double.NaN;
            }
        }

        public MovingAverage(int windowSize)
        {
            _windowSize = windowSize;
            _samples = new Queue<double>(windowSize);
        }

        public void AddSample(double newSample)
        {
            if (!double.IsNaN(newSample))
            {
                if (_samples.Count == _windowSize)
                {
                    _sum -= _samples.Dequeue();
                }
                _samples.Enqueue(newSample);
                _sum += newSample;
            }
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Enhancement RequestedProduct code improvement that does NOT require public API changes/additions

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions