Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Improving the WPF DispatcherTimer Precision

5.00/5 (6 votes)
3 Feb 2022Public Domain4 min read 13.6K   125  
How to correct the problem that the DispatcherTimer raises the Tick events slower than required by Interval
WPF uses the DispatcherTimer to raise the Tick. The time between 2 ticks should always be the same, defined by DispatcherTimer.Interval. The advantage of using the DispatcherTimer is that the event is raised on the WPF thread. The disadvantage is that the WPF threat has also other things to do, which might take hundreds of milliseconds. If Interval is 1 second or shorter, the ticks are raised slower than as they should be. This article explains how changing the interval tick by tick can improve the regularity of the ticks.

Introduction

I wrote an open source WPF game MasterGrab, which is running successfully since 6 years. In this game, a human player plays against several robots. Lately, I wanted to extend the game so that the robots can play among themselves. The user can choose how many moves per second the robots should make, each move displayed on the screen.

I thought that would be easy to implement using the DispatcherTimer, which raises a Tick event on the WPF thread every x second. This duration is controlled by DispatcherTimer.Interval. When I set it to 100 milliseconds, I noticed that I saw only about 4 moves a second at irregular times. So I started to investigate how the DispatcherTimer behaves and how that can be improved.

Minimal Time WPF Needs between 2 Ticks

Well, the first question is: How fast can the DispatcherTimer run ? To measure that on my PC, I wrote a WPF application which is not doing anything except running a DispatcherTimer. The XAML Window contains only a TextBlock with the Name MainTextBlock:

C#
using System;
using System.Text;
using System.Windows;
using System.Windows.Threading;

namespace WpfTimer {
  public partial class MainWindow: Window {
    DispatcherTimer timer;

    public MainWindow() {
      InitializeComponent();

      timer = new();
      timer.Interval = TimeSpan.FromMilliseconds(0);
      timer.Tick += Timer_Tick;
      timer.Start();
    }

    const int timesCount = 20;
    DateTime[] times = new DateTime[timesCount];
    int timesIndex;

    private void Timer_Tick(object? sender, EventArgs e) {
      times[timesIndex] = DateTime.Now;
      if (++timesIndex>=timesCount) {
        timer.Stop();
        var sb = new StringBuilder();
        var startTime = times[0];
        for (int i = 1; i < timesCount; i++) {
          var time = times[i];
          sb.AppendLine($"{(time - startTime):ss\\.fff} | 
                       {(int)(time - times[i-1]).TotalMilliseconds, 3:##0}");
        }
        MainTextBox.Text = sb.ToString();
      }
    }
  }
}

The output was:

00.021 |  21
00.021 |   0
00.021 |   0
00.021 |   0
...

For each tick, there is one line. The first column shows how many seconds.milliseconds after the first tick this tick occurs. The second column shows how much time passed between this tick and the previous tick.

Since I set the Interval to 0, there is no time lost between ticks, except between the very first tick and the second one. Obviously, setting the Interval to 0 doesn't make sense and I would have expected an exception.

Here is the result for Interval = 1 millisecond:

00.192 | 192
00.215 |  22
00.219 |   3
00.235 |  15
00.471 | 236
00.600 | 128
00.743 | 142
00.764 |  21
00.935 | 170
01.239 | 303
01.326 |  87
01.628 | 302
01.894 | 266
02.210 | 316
02.375 | 164
02.435 |  60
02.527 |  92
02.658 | 131
02.685 |  26

I did not expect that Tick would be raised every millisecond, but I am surprised to find that over 300 milliseconds might pass between 2 ticks, considering that my application is doing exactly nothing except running the timer.

Increasing the DispatcherPriority used for the DispatcherTimer

Then I noticed that the DispatcherTimer constructor can take a Dispatcher<wbr />Priority parameter, which seems to be set to Background, which means the timer will only run once "all other non-idle operations are completed".

Name      Priority Description
Invalid         -1 This is an invalid priority.
Inactive         0 Operations are not processed.
SystemIdle       1 Operations are processed when the system is idle.
ApplicationIdle  2 Operations are processed when the application is idle.
ContextIdle      3 Operations are processed after background operations have completed.
Background       4 Operations are processed after all other non-idle operations are completed.
Input            5 Operations are processed at the same priority as input.
Loaded           6 Operations are processed when layout and render has finished but just 
                   before items at input priority are serviced. Specifically this is used 
                   when raising the Loaded event.
Render           7 Operations processed at the same priority as rendering.
DataBind         8 Operations are processed at the same priority as data binding.
Normal           9 Operations are processed at normal priority. This is the typical 
                   application priority.
Send            1o Operations are processed before other asynchronous operations. This is 
                   the highest priority.

I tried in my game application which highest priority I can use. Obviously, rendering has to be finished before it makes sense to display the next move. So let's see how quickly Tick gets raised when using Dispatcher<wbr />Priority.Input:

00.014 |  14
00.024 |   9
00.091 |  67
00.202 | 111
00.221 |  19
00.226 |   4
00.242 |  16
00.272 |  30
00.307 |  34
00.369 |  61
00.460 |  91
00.493 |  33
00.524 |  30
00.555 |  31
00.586 |  30
00.712 | 125
00.745 |  33
00.761 |  15
00.788 |  27

Choosing a Realistic Duration for Interval

I would say it can run nearly 3 times faster now. Obviously, Interval=1millisecond doesn't really make sense. So how about Interval=100 msec?

00.193 | 193
00.292 |  98
00.417 | 124
00.592 | 174
00.718 | 126
00.876 | 157
01.001 | 125
01.142 | 141
01.263 | 120
01.392 | 129
01.559 | 166
01.677 | 117
01.872 | 195
02.010 | 137
02.143 | 133
02.256 | 113
02.358 | 101
02.472 | 114
02.589 | 116

Oops, now the time needed between 2 ticks is nearly always significantly more than 100 msec and I get only about 7 instead of 10 ticks per second. :-( The problem seems to be that after a random delay of x milliseconds, the timer waits again 100 msec, instead 100-x milliseconds.

Making the Tick to Get Raised Reliably every 100 msec

So basically, we have to tell during each tick event how many msec long the Interval should be until the next tick:

C#
const int constantInterval = 100;//milliseconds

private void Timer_Tick(object? sender, EventArgs e) {
  var now = DateTime.Now;
  var nowMilliseconds = (int)now.TimeOfDay.TotalMilliseconds;
  var timerInterval = constantInterval - 
   nowMilliseconds%constantInterval + 5;//5: sometimes the tick comes few millisecs early
  timer.Interval = TimeSpan.FromMilliseconds(timerInterval);

The code works like this. It tries to have a tick exactly every 0.1 second. So if the first tick occurs after 142 msec, the Interval gets set to 58 msec instead of 100:

00.093 |  93
00.216 | 122
00.311 |  95
00.408 |  96
00.515 | 106
00.611 |  96
00.730 | 119
00.859 | 128
00.929 |  70
00.995 |  65
01.147 | 152
01.209 |  62
01.314 | 104
01.402 |  87
01.496 |  94
01.621 | 125
01.731 | 109
01.794 |  63
01.936 | 141

Finally! Now I have 10 ticks per second. Of course, it is not precisely every 100 msec, because sometimes, the WPF thread still needs too much time for other activities. But when that happens, the timer tries at least to raise the next tick faster.

About My Game

I wrote MasterGrab 6 years ago and since then, I play it nearly every day before I start programming. It takes about 10 minutes to beat 3 robots who try to grab all 300 countries on a random map. The game finishes once one player owns all the countries. The game is fun and every day fresh because the map looks completely different. The robots bring some dynamic into the game, they compete against each other as much as against the human player. If you like, you can even write your own robot, the game is open source. I wrote mine in about 2 weeks, but I am surprised how hard it is to beat them. When playing against them, one has to develop a strategy so that the robots attack each other instead of you. I will write a CodeProject article about it sooner or later, but you can already download and play it, there is good help in the application explaining how to play:

History

  • 3rd February, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication