Wednesday, January 21, 2015

WPF Frame Rate Calculator

I've been playing around with WPF's CompositionTarget.Rendering event.
From the documentation:

CompositionTarget.Rendering Occurs just before the objects in the composition tree are rendered.The Rendering event is routed to the specified event handler after animation and layout have been applied to the composition tree.
From this event, I've been looked at a way to calculate how many frames are being rendered per second in a WPF application.

Caveat Emptor:
  1. CompositionTarget.Rendering is a static member, so remember to unhook your delegate when you don't need it...leaks....
  2. The act of adding a delegate to CompositionTarget.Rendering might slow your WPF application down...so it's probably best used debugging performance, or as a user configurable options when used in a production environment.  
  3. My calculations aren't very scientific..but the numbers produced are close to those seen by attaching to the application with Perforator from WPF Performance Suite.
This could be handy if a low frame rate is detected, you could log the time stamp (using something like log4net) and compare what was going on in the rest of you app.



Links:
From the WPF Peformance Suite, frame rate:
For applications without animation, this value should be near 0. During animations in a well-performing application, Frame Rate should be close to the monitor’s refresh rate (typically 60 or 75). 

WPF applications start off with two main threads; one hidden background thread for rendering and one to manage the user interface.


I've created a FrameRateCalculator class that hooks into CompositionTarget.Rendering and calculates the current rate, once per second. Using Reactive Extensions there is an Observable property which I subscribe to in order to receive frame rate updates. You'll need to run the following command into NuGet package manager console in Visual Studio:


Install-Package Rx-PlatformServices
FrameRateCalculator uses a couple of basic multithreading practices to ensure data is accurate (NB these could have been improved further - but for now I'll stick with basic CompareAndSwap operations).

Full Source:


The frame rate's details are wrapped into single class (struct..for next version):
class FrameRate
{
    public FrameRate(string time, int frames)
    {
        Time = time;
        Frames = frames;
    }
 
    public string Time { getprivate set; }
    public int Frames { getprivate set; }
}
A string has been used to identify the Time as a convenience for the WPF graphing tool that I've used for the demo - ideally this could be a TimeSpan - as I actually use DateTime.Now to determine the 'time'. The guts of FrameRateCalculator:
using System;
using System.Diagnostics;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Windows.Media;
class FrameRateCalculator : IDisposable
{
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
 
    private readonly ISubject<FrameRate> _rateSubject = new Subject<FrameRate>();
 
    private readonly Stopwatch _frameRateCalcStopWatch = new Stopwatch();
    private int _frameCount;
    private int _previousFrameCount;
 
    private readonly Timer _frameRateCalcTimer;
 
    private const int IsNotRunning= 0;
    private const int IsRunning = 1;
    private int _isRunning = IsNotRunning;
 
    public FrameRateCalculator()
    {
        _frameRateCalcTimer = new Timer(CalculateFrameRate, nullTimeSpan.Zero, TimeSpan.Zero);
    }
 
    public void Start()
    {
        if (Interlocked.CompareExchange(ref _isRunning, IsRunning, IsNotRunning) == IsNotRunning)
        {
            _frameRateCalcStopWatch.Start();
            _frameRateCalcTimer.Change(TimeSpan.Zero, TimeSpan.FromSeconds(1));
            CompositionTarget.Rendering += CompositionTargetRendering;
        }
    }
 
    private void CompositionTargetRendering(object sender, EventArgs e)
    {
        _lock.EnterWriteLock();
        try
        {
            _frameCount++;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }
 
    public void Stop()
    {
        StopOrReset(true);
    }
 
    public void Reset()
    {
        StopOrReset(false);
    }
 
    public IObservable<FrameRate> Observable
    {
        get { return _rateSubject.AsObservable(); }
    }
 
    private void StopOrReset(bool stop)
    {
        if (Interlocked.CompareExchange(ref _isRunning, IsNotRunning, IsRunning) == IsRunning)
        {
            _frameRateCalcTimer.Change(TimeSpan.Zero, TimeSpan.Zero);
 
            if (stop)
                _frameRateCalcStopWatch.Stop();
            else
                _frameRateCalcStopWatch.Reset();
 
            CompositionTarget.Rendering -= CompositionTargetRendering;
        }
    }
 
    private void CalculateFrameRate(object state)
    {
        int framesThisTick;
 
        _lock.EnterReadLock();
        try
        {
            var tempFrameCount = _frameCount;
            framesThisTick = (_frameCount - _previousFrameCount);
            _previousFrameCount = tempFrameCount;
        }
        finally
        {
            _lock.ExitReadLock();
        }
 
        var tickTime = _frameRateCalcStopWatch.Elapsed.ToString(@"mm\:ss");
        var frameRate = new FrameRate(tickTime, framesThisTick);
        _rateSubject.OnNext(frameRate);
    }
 
    public void Dispose()
    {
        _frameRateCalcTimer.Dispose();
    }
}

The constructor sets up  a Timer (from System.Threading namespace).  The callback  CalculateFrameRate gets  called by a ThreadPool thread so needs to be re-entrant..although this re-entrancy is unlikely to be a problem as it's called only once each second.  

I dont want to use a WPF thread as that would skew my calculations.

The Start() method uses  Interlocked.CompareExchange to ensure that we're not adding multiple delegates to CompositionTarget.Rendering - still will start the process of lsitneign for render calls, calculating the current frame rate and signal anyone listening via IObservable<FrameRate> Observable.
Start()  adds the delegate CompositionTargetRendering:
CompositionTarget.Rendering += CompositionTargetRendering;
private void CompositionTargetRendering(object sender, EventArgs e)
{
    _lock.EnterWriteLock();
    try
    {
        _frameCount++;
    }
    finally
    {
        _lock.ExitWriteLock();
    }
}
Each time WPF tells us that  a rendering operation has occurred, we increment the frameCount value.
The frame rate calculation code is found in CalculateFrameRate(object state):
private void CalculateFrameRate(object state)
{
    int framesThisTick;
 
    _lock.EnterReadLock();
    try
    {
        var tempFrameCount = _frameCount;
        framesThisTick = (_frameCount - _previousFrameCount);
        _previousFrameCount = tempFrameCount;
    }
    finally
    {
        _lock.ExitReadLock();
    }
 
    var tickTime = _frameRateCalcStopWatch.Elapsed.ToString(@"mm\:ss");
    var frameRate = new FrameRate(tickTime, framesThisTick);
    _rateSubject.OnNext(frameRate);
}
It's pretty simple, using a ReaderWriterLockSlim lock to enable thread safe access, we  determine  the number of frames that have been rendered in the last second.

The  Stop() method safely stops the frame rate calculations.
To use the calculator, you'll need to call Start() and subscribe to the observable sequence like this:

var frameRates = new ObservableCollection<FrameRate>();
var calculator = new FrameRateCalculator();
calculator.Start()
var frameSubscription = calculator.Observable
    .ObserveOn(SynchronizationContext.Current)
    .Subscribe(fr =>
{
    frameRates.Add(fr);
});
I'm storing each of the FrameRate items received in an ObservableCollection<FrameRate> so I need to use RX's  ObserveOn() extension method to ensure the WPF Dispatcher thread adds to the ObservableCollection.
To demo this in WPF, I've created a ViewModel FrameRateViewModel which I'll bind to in a moment
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Windows.Input;
 
class FrameRateViewModel : INotifyPropertyChangedIDisposable 
{
    private readonly FrameRateCalculator _calculator = new FrameRateCalculator(); 
    private IDisposable _frameSubscription; 
    public FrameRateViewModel()
    {
        FrameRates = new ObservableCollection<FrameRate>(); 
        StartCommand = new RelayCommand(_ => _calculator.Start());
        StopCommand = new RelayCommand(_ => _calculator.Stop());
        ResetCommand = new RelayCommand(_ =>
        {
            FrameRates.Clear();
            _calculator.Reset();
        });
 
        _frameSubscription = _calculator.Observable
            .ObserveOn(SynchronizationContext.Current)
            .Subscribe(fr =>
        {
            FrameRates.Add(fr);
            if (FrameRates.Count > 30)
            {
                FrameRates.RemoveAt(0);
            }
            CurrentFrameRate = fr.Frames;
        });
    }
 
    public ObservableCollection<FrameRate> FrameRates { getprivate set; }
 
    private int _currentFrameRate;
    public int CurrentFrameRate
    {
        get { return _currentFrameRate; }
        set
        {
            if (_currentFrameRate != value)
            {
                _currentFrameRate = value;
                NotifyPropertyChanged();
            }
        }
    }
 
    public ICommand StopCommand { getprivate set; }
    public ICommand StartCommand { getprivate set; }
    public ICommand ResetCommand { getprivate set; }
 
    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged([CallerMemberNamestring propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(thisnew PropertyChangedEventArgs(propertyName));
    }
 
    public void Dispose()
    {
        _frameSubscription.Dispose();
        _calculator.Dispose();
    }
}
The observable subscription ensures that we have only a maximum of 30 ticks (for the last 30 seconds)...this make the demo screen's chart look similar to that in Perforator:

_frameSubscription = _calculator.Observable
    .ObserveOn(SynchronizationContext.Current)
    .Subscribe(fr =>
{
    FrameRates.Add(fr);
    if (FrameRates.Count > 30)
    {
        FrameRates.RemoveAt(0);
    }
    CurrentFrameRate = fr.Frames;
});

I'm using MVVM practices here, so Stop, Start and Reset commands are based on the now common  RelayCommand:

public class RelayCommand : ICommand
{
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;
 
    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {}
 
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
 
        _execute = execute;
        _canExecute = canExecute;
    }
 
    public bool CanExecute(object parameter)
    {
        return _canExecute == null || _canExecute(parameter);
    }
 
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
 
    public void Execute(object parameter)
    {
        _execute(parameter);
    }
}
My WPF demo app has a single WPF Window which uses a Chart control from the WPF Toolkit.  You'll need to install the WPF Tool kit from here from if you want to use the following XAML.

NB: I'm mixing WPF data binding with a little bit code behind.  I wouldn't normally use code behind...but it's only a demo ;)

1.  Add a new window called MainWindow.xaml:

<Window x:Class="BlogFrameRate.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:chartToolkit="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"
        Title="MainWindow" Height="700" Width="700">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
 
        <StackPanel Grid.Column="0" Grid.Row="0" Orientation="Horizontal">
            <Button Content="Add One" Padding="4" Margin="8" Click="Button_Click"/>
            <Button Content="Add Many" Padding="4" Margin="2,8" Click="AddManyClick" />
            
            <Label Content="Items:" VerticalAlignment="Center" Margin="2,8"/>
            <Label Name="ItemCountLabel" Margin="-4,8,4,8" VerticalAlignment="Center"  />
            
            <CheckBox Content="Delay" VerticalAlignment="Center" Margin="8" Name="CheckBoxDelay" 
                      Click="CheckBoxDelay_Click"/>
            <Slider VerticalAlignment="Center" Width="100" Padding="4" Margin="-4,8" Name="Slider" 
                    AutoToolTipPlacement="BottomRight" 
                    ValueChanged="Slider_ValueChanged" SmallChange="0.01" TickFrequency="100" Maximum="100"/>
 
            <Label Content="Frame Rate:" Margin="8" VerticalAlignment="Center" />
            <Label Content="{Binding CurrentFrameRate, FallbackValue=na}" VerticalAlignment="Center" MinWidth="20" />
 
            <Button Content="Start" Padding="4" Margin="4,8" Command="{Binding StartCommand}"/>
            <Button Content="Stop" Padding="4" Margin="4,8" Command="{Binding StopCommand}"/>
            <Button Content="Reset" Padding="4" Margin="4, 8" Command="{Binding ResetCommand}"/>
        </StackPanel>
 
        <chartToolkit:Chart Grid.Column="0" Grid.Row="1" Padding="4" >
 
            <chartToolkit:Chart.LegendStyle>
                <Style TargetType="Control">
                    <Setter Property="Width" Value="0"/>
                </Style>
            </chartToolkit:Chart.LegendStyle>
                
            <chartToolkit:AreaSeries DependentValuePath="Frames" IndependentValuePath="Time" 
                                     ItemsSource="{Binding FrameRates}" >
                <chartToolkit:AreaSeries.IndependentAxis>
                    <chartToolkit:CategoryAxis Orientation="X"  />
                </chartToolkit:AreaSeries.IndependentAxis>
            </chartToolkit:AreaSeries>
 
        </chartToolkit:Chart>
 
        <ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Column="0" Grid.Row="2">
            <WrapPanel   Orientation="Horizontal" Name="ImagePanel" />
        </ScrollViewer >
    </Grid>
</Window>
2. Open the MainWindow.xaml.cs file and replace with the following code.  The Add buttons add images to the image panel. Each image uses  a  DoubleAnimationUsingKeyFrames to rotate the image (the spins slow down when you use the GUI delay code, described later):
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
 
namespace BlogFrameRate
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly FrameRateViewModel _viewModel = new FrameRateViewModel();
        private readonly GuiDelay _delayer;
        
        public MainWindow()
        {
            InitializeComponent();
 
            _delayer = new GuiDelay(Dispatcher);
            DataContext = _viewModel;
        }
 
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            AddImage();
        }
 
        private void AddImage()
        {
            var bitmapImage = new BitmapImage();
            bitmapImage.BeginInit();
            bitmapImage.UriSource = new Uri(@"max.jpg"UriKind.RelativeOrAbsolute);
            bitmapImage.EndInit();
 
            var imageControl = new Image
            {
                Source = bitmapImage,
                Stretch = Stretch.None,
                RenderTransform = new RotateTransform(),
                RenderTransformOrigin = new Point(0.5D, 0.5D)
            };
 
            //var angleAnimation = CreateAnimation();
            var angleAnimation = CreateAnimationKeyFrame();
            imageControl.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, angleAnimation);
            ImagePanel.Children.Add(imageControl);
 
            ItemCountLabel.Content = ImagePanel.Children.Count;
        }
 
        private static DoubleAnimation CreateAnimation()
        {
            return new DoubleAnimation
            {
                 From = 0,
                To = 360,
                Duration = new Duration(TimeSpan.FromSeconds(1)),
                RepeatBehavior = RepeatBehavior.Forever
            };
        }
 
        private static DoubleAnimationUsingKeyFrames CreateAnimationKeyFrame()
        {
            var angleAnimation = new DoubleAnimationUsingKeyFrames
            {
                Duration = new Duration(TimeSpan.FromSeconds(1)),
                RepeatBehavior = RepeatBehavior.Forever
            };
 
            angleAnimation.KeyFrames.Add(
                new LinearDoubleKeyFrame(360, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1))) );
 
            return angleAnimation;
        }
 
        private void AddManyClick(object sender, RoutedEventArgs e)
        {
            for (var i = 0; i < 20; i++)
            {
                AddImage();
            }
        }
 
        private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            _delayer.Duration = TimeSpan.FromMilliseconds(e.NewValue);
        }
 
        private void CheckBoxDelay_Click(object sender, RoutedEventArgs e)
        {
            _delayer.IsActive = CheckBoxDelay.IsChecked.GetValueOrDefault();
        }
 
    }
}
3. You'll need to add an image Resource called max.jpg - in the demo app I've included an image 91/132 pixels of my dog Max.

4. Finally, add this delay class.  All this does is hook into the Dispatcher thread and inject a delay as set by the slider on the main window.  You don't need to implement this but, but it serves as at way to slow down the actual frame rate
using System;
using System.Threading;
using System.Windows.Threading;
 
namespace BlogFrameRate
{
    class GuiDelay : IDisposable
    {
        private readonly Timer _timer;
        private readonly Dispatcher _dispatcher;
 
        public GuiDelay(Dispatcher dispatcher)
        {
            Duration = TimeSpan.Zero;
            _dispatcher = dispatcher;
 
            _timer = new Timer(BlockGuiThread, nullTimeSpan.Zero, TimeSpan.FromMilliseconds(100));
        }
 
        public TimeSpan Duration { getset; }
        public bool IsActive { getset; }
 
        private bool _isInDelayLoop;
        private void BlockGuiThread(object state)
        {
            if (IsActive)
            {
                _dispatcher.Invoke(() =>
                {
                    if (_isInDelayLoop) return;
                    _isInDelayLoop = true;
                    Thread.Sleep(Duration);
                    _isInDelayLoop = false;
                });
            }
        }
 
        public void Dispose()
        {
            _timer.Dispose();
        }
    }
}
Run the app, create a few spinning images, and press Start you should see a chart of frame rates that match those that you'd see if you attach WPF Performance Suite's Perforator to the running app at the same time.



No comments: