Multimedia-Timer icon indicating copy to clipboard operation
Multimedia-Timer copied to clipboard

Updated to NET 6.0 and fixed Initialization.

Open gmgallo opened this issue 1 year ago • 1 comments

Hi @MikeCodesDotNET This is a nice little Multimedia timer wrapper. As it was mentioned in other post the Initialization has some flaws that was easy to fix: period was already initialized by the Interval property, and Resolution must be set with Capabilities.PeriodMin. Here is the snippet with the correction needed:

        private void Initialize()
        {
            mode = TimerMode.Periodic;
  //          period << already initialized by the Interval property
            Resolution = TimeSpan.FromMilliseconds(Capabilities.PeriodMin);
            IsRunning = false;
            timeProcPeriodic = new TimeProc(PeriodicEventCallback);
            timeProcOneShot = new TimeProc(OneShotEventCallback);
            tickRaiser = new TickDelegate(OnTick);
        }

In my project I need a very precise timer in the order of the 10ms, so I went ahead and modified the test program to measure the actual accuracy and repeatability of this MM Timer using the highly accurate Stopwatch from System.Diagnostics. Here is the modified test program, and below some test results in my intel i7 machine.

using System;
using MultimediaTimer;
using System.Diagnostics;
using System.Threading.Tasks;

namespace DemoApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Multimedia Timer Test\n\n");

            var timer = new Timer();

            Console.WriteLine($"Timer resolution: {timer.Resolution} \nMin period: {timer.MinPeriod}ms \n" +
                $"Max period: {timer.MaxPeriod}ms\nStopwatch Frequency: {Stopwatch.Frequency/1000}KHz");

            Console.WriteLine("\nType PERIOD in milliseconds<Enter>");

            string line = Console.ReadLine();


            double period;

            if (!double.TryParse(line, out period) ) 
            { 
                Console.WriteLine($"Not a valid period: {line}"); return; 
            }

            Interval = period;

            timer = new(period); 
            timer.Elapsed += Timer_Tick;
            timer.Resolution = TimeSpan.FromMilliseconds(2);

            sw = Stopwatch.StartNew();
            timer.Start();

            Task.Delay((int)period*capacity + 1000).Wait();

            timer.Stop();                   
    
            PrintStats();   
        }

        static double Interval;
        static Stopwatch sw; 
        static int capacity = 1000;
        static int index = 0;

        static double[] Q = new double[capacity]; // can't use generics Queue in static fields.

        private static void Timer_Tick(object sender, EventArgs e)
        {
            double t = 1000.0D * sw.ElapsedTicks / Stopwatch.Frequency;

            Q[index++ % capacity] = t;

            Console.WriteLine($"Timer Ticked: {DateTime.Now.ToString("hh:mm:ss.fff")} - Stopwatch: {t: 0.00}");
            sw.Restart();
        }

        static void PrintStats()
        {
            double Max = double.MinValue;
            double Min = double.MaxValue;
            double Avg = 0;
            double cnt = Q.Length;  // assume that we have full Queue.

            foreach (var t in Q) 
            {
                if (t > Max)
                    Max = t;
                if (t < Min)
                    Min = t;

                Avg +=  t / cnt;
            }

            double ss = 0;

            foreach (var t in Q)
            {
                var s = t - Avg;
                ss += s * s;
            }
             
            double Std = Math.Sqrt(ss/cnt);

            Console.WriteLine($"Interval     : {Interval}");
            Console.WriteLine($"Samples      : {cnt}");
            Console.WriteLine($"Max value    : {Max:0.000}");
            Console.WriteLine($"Min value    : {Min:0.000}");
            Console.WriteLine($"Value Spread : {Max-Min:0.000}");
            Console.WriteLine($"Average      : {Avg:0.000}");
            Console.WriteLine($"Std Deviation: {Std:0.000}");
        }
    }
}

The program asks the user for a time interval and then waits for the necessary time to fill the 1000 samples array, then prints the statistics. Here are my results for a 100, 10, and 5 ms itervals.

Interval     : 100
Samples      : 1000
Max value    : 100.924
Min value    : 98.694
Value Spread : 2.230
Average      : 99.785
Std Deviation: 0.424

Interval     : 10
Samples      : 1000
Max value    : 10.936
Min value    : 7.279
Value Spread : 3.657
Average      : 9.770
Std Deviation: 0.489

Interval     : 5
Samples      : 1000
Max value    : 6.363
Min value    : 2.412
Value Spread : 3.951
Average      : 4.781
Std Deviation: 0.461

Unfortunately, it won't work for me with a variation of more than 3ms @ 10ms interval, it turns out that is not any better than the .NET System.Timers.Timer which also runs in the thread pool and has variations in the same order of magnitude. This Multimedia timer is using deprecated Win32 interfaces. It would be good to see if using the new Multimedia Class Scheduler Service a higher accuracy could be achieved, but that is left as an exercise for the reader, that is another way to say that I don't have time to doit right now :-).

gmgallo avatar May 16 '24 22:05 gmgallo

UPDATE To back up my words with actual results I went ahead and ran the same test against System.Timers.Timer. To my surprise the results are much worse than what the literature says, so your efforts are not totally wasted.

 #======================  System.Timers.Timer Results:
Interval     : 10
Samples      : 1000
Max value    : 61.636
Min value    : 1.333
Value Spread : 60.302
Average      : 14.159
Std Deviation: 3.758

Then I thought why not to use the accurate Stopwatch as a time base which has a 10 MHz clock (in my Windows 10 system at least). The results are impressive:

#======================  Stopwatch Test Results:
Interval     : 10
Samples      : 1000
Max value    : 11.230
Min value    : 10.000
Value Spread : 1.230
Average      : 10.002
Std Deviation: 0.039

What about 1ms delay?

#======================  Stopwatch Test Results:
Interval     : 1
Samples      : 1000
Max value    : 1.020
Min value    : 1.000
Value Spread : 0.020
Average      : 1.000
Std Deviation: 0.001

Wow!, I think that this is runing inside a single CPU time slice and therefore the accuracy is much higher. This is a brute force approach that burns CPU cycles, but it is justified in my case. Here is the simple code behind these results in case someone needs this level of accuracy:

        class StopwatchDelay
        {
            public void Delay(long msec)
            {
                long ticsPerMsec = Stopwatch.Frequency / 1000;

                var sw = Stopwatch.StartNew();
                long timeout = msec * ticsPerMsec;

                while (sw.ElapsedTicks < timeout)       // CPU burner!
                    ;
            }
        }

        static async Task RunStopwatchTest()
        {
            Console.WriteLine("\nStopwatch Timer test ...");

            bool stop = false;

            var task = Task.Run(() => 
            { 
               var d = new StopwatchDelay();

                while (!stop) 
                {  
                    d.Delay((long)Interval);
                    Timer_Tick(null, null);
                }
            });

            int totaldelay = (int)Interval * capacity + 1000;

            do
            {
                Console.Write($"\rTime remaining: {totaldelay / 1000,3}");
                Task.Delay(1000).Wait();
                totaldelay -= 1000;

            } while (totaldelay > 0);

            stop = true;
            await task;

            PrintStats("\n\n#======================  Stopwatch Test Results:");
        }

gmgallo avatar May 17 '24 00:05 gmgallo