C# Process wait millisecond precise

1.7k Views Asked by At

I am developing an application (sort of a game helper), which sends keystrokes to a game at certain time intervals (you can specify what key will be pressed).

The problem is that I need to do the KeyPress event with a millisecond precision. After some research I found out that Thread.Sleep has a resolution of 20-50 ms and the best I could find so far was using the StopWatch() like following:

cmd_PlayJumps = new DelegateCommand(
    () =>
    {
        ActivateWindow();

        Stopwatch _timer = new Stopwatch();
        Stopwatch sw = new Stopwatch();
        double dElapsed = 0;

        //Initial Key Press
        _timer.Start();
        _Keyboard.KeyPress(WindowsInput.Native.VirtualKeyCode.RETURN);

        int iTotalJumps = SelectedLayout.JumpCollection.Count;
            //loop through collection
            for (int iJump = 0; iJump < iTotalJumps - 1; iJump++)
            {
                dElapsed = _timer.Elapsed.TotalMilliseconds;

                sw.Restart();
                while (sw.Elapsed.TotalMilliseconds < SelectedLayout.JumpCollection[iJump + 1].WaitTime -
                    dElapsed)
                {
                    //wait
                }

                _timer.Restart();
                _Keyboard.KeyPress(WindowsInput.Native.VirtualKeyCode.RETURN);
            }

            //final key press
            _Keyboard.KeyPress(WindowsInput.Native.VirtualKeyCode.RETURN);

            _timer.Stop();
            _timer = null;
});

As duration of KeyPress event varies within 0.3 - 1.5 ms I also keep track of that in order to get rid of deviation.

Nonetheless, I am only able to get 60% accuracy with this code, as even the StopWatch() is not that precise (of course if my code is not incorrect).

I would like to know, how can I achieve at least 90% accuracy?

3

There are 3 best solutions below

1
On

You could try using ElapsedTicks. It is the smallest unit that the Stopwatch can measure and you can convert the number of elapsed ticks to seconds (and fractions of seconds of course) using the Frequency property. I do not know if it is better than Elapsed.TotalMilliseconds but worth a try.

0
On

The Problem is that you need to be lucky, it all depends on how often the .Tick is reached, which will be around 0.2-2 ms depending on your hardware. It is extremely difficult to avoid this, you could try setting a high process priority to steal the CPU away to get more Ticks in.
This can be achieved with :

  System.Diagnostics.Process.GetCurrentProcess().PriorityClass =     ProcessPriorityClass.High;         

Also try setting

  while (sw.Elapsed.TotalMilliseconds <= SelectedLayout.JumpCollection[iJump + 1].WaitTime - dElapsed)

Which should save you another tick sometimes and get the accuracy up a little.

Other than that the Main issue is that windows itself is not the best Timekeeper, DateTime.Now has a tolerance of 16ms for instance and was never thought of as a "real time" operating system.

As a side note :If you really need this to be as accurate as possible I'd advise you to look into Linux.

0
On

I got it to an average timing miss of 0.448 milliseconds using a combination of Thread.Sleep and a spin waiter. Setting the thread to high priority does not change the logic, as the thread needs to be running and continuously checking the variable.

static void Main(string[] args)
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    var timespans = new List<TimeSpan>(50);
    while (timespans.Count < 50)
    {
        var scheduledTime = DateTime.Now.AddSeconds(0.40);
        Console.WriteLine("Scheduled to run at: {0:hh:mm:ss.FFFF}", scheduledTime);
        var wait = scheduledTime - DateTime.Now + TimeSpan.FromMilliseconds(-50);
        Thread.Sleep((int)Math.Abs(wait.TotalMilliseconds));
        while (DateTime.Now < scheduledTime) ;
        var offset = DateTime.Now - scheduledTime;
        Console.WriteLine("Actual:              {0}", offset);
        timespans.Add(offset);

    }
    Console.WriteLine("Average delay: {0}", timespans.Aggregate((a, b) => a + b).TotalMilliseconds / 50);
    Console.Read();
}

Please note that true realtime code cannot be obtained using standard, running on windows, CLR code. The garbage collector could step in, even in between loop cycles, and start collecting objects, at which point have a good chance of getting imprecise timing.

You can reduce the chance of this happening by changing the garbage collector's Latency Mode, at which point it won't do the big collections until extreme low memory situations. If this is not enough for you, consider writing the above solution in a language where better guarantees for timing are given (e.g. C++).