chriskohlhoff / asio

Asio C++ Library
http://think-async.com/Asio
4.73k stars 1.2k forks source link

Support high resolution waitable timers on Windows #1328

Open mtnpke opened 1 year ago

mtnpke commented 1 year ago

Windows traditionally uses a timer resolution of roughly 15.6 ms. This means that all timing-related functionality, such as waitable timers (that asio uses internally to implement all types of timers) or the Sleep() call, is limited to this resolution. If you call Sleep(1) or set a timer to expire after 1 ms and wait on it in a loop, the actual duration of the wait will be around 15.6 ms.

This might be sufficient for most programs, but there are use cases regarding, for example, real-time audio/video processing or communication, that can benefit vastly from a better timer resolution. Windows allows to improve the resolution by two ways:

What do you think about supporting the HIGH_RESOLUTION flag in some fashion? I'm willing to put some effort into a PR if I get some direction on how this should behave from an API point of view. I'd certainly like to use that more in my application than having to resort to timeBeginPeriod(). Other projects such as Python are also beginning to support this API, see this commit for example.

NostraMagister commented 6 months ago

The standard C++ library does not force an implementation to effectively provide nanosecond level timing and allows the high resolution clock to be mapped to, say, the steady clock. See NOTE in https://en.cppreference.com/w/cpp/chrono/high_resolution_clock

For instance, Windows and Linux both bind the high resolution clock to a lower resolution clock. Implementing better resolution involves OS specific approaches, e.g. in Linux the POSIX signal based timing can be used. In Windows the CREATE_WAITABLE_TIMER_HIGH_RESOLUTION flags as mentioned by OP.

Because the Asio library is based on the underlying Standard C++ chrono it suffers the problem of the NOTE in the link above.

Indeed in practice timing video frames at 30 fps, 33ms per frame, works for all operating systems, behind that an alternative timer system must be used. 60 fps (about 16ms) works on Linux but becomes unreliable on Windows, 90 fps, 11ms, (needed for 3D headsets to avoid nausea) or 120 fps, 8 ms, for the perfect 3D user experience requires sub 10ms timing and become unreliable on all Operating Systems and requires alternative timer code.

The suggestion of supporting the CREATE_WAITABLE_TIMER_HIGH_RESOLUTION flag is a Windows solution. Possibly the POSIX solution using signals might be a more cross-platform approach and signals are already supported in ASIO. It would, IMO, fit nicely into Asio's existing Async callback model.

I have been looking at https://man7.org/linux/man-pages/man2/clock_gettime.2.html and related functions. If Windows Posix support would be insufficient (it may not support all clock types such as raw monotonic) then an #ifdef approach can combine it with the suggested CREATE_WAITABLE_TIMER_HIGH_RESOLUTION flag. I am not even sure if the Asio user needs to specify that flag because setting it is inherent to using the high resolution clock type.

Asio's Timers ABI would not be broken, because in both cases, it's under the hood work, and users that already use the high resolution clock will never complain if that the clock suddenly gained more accuracy. One uses that clock if the accuracy is needed.

And on a completely separate note, I also don't understand why the C++ standard did not require the high precision clock to be nanosecond level, or at least have a function allowing to retrieve the clocks exact precision. In times where multimedia becomes increasingly important I think its time timer support is tackled at the standard level. Even a simple phone has better then millisecond timing available at the hardware level (the tick counter level). AIMO.

NostraMagister commented 6 months ago

I forgot to mention timer_create() and timer_settime() functions with the CLOCK_MONOTONIC or CLOCK_REALTIME clock IDs. that should be able to provide nanosecon level timing on Linux and others supporting the POSIX API.

mtnpke commented 6 months ago

Thanks for your thoughts. What exactly do you mean by "Windows Posix support"? AFAICT, WinAPI does not support any of the POSIX clock and timer APIs. In consequence, using timer_create would be as much a POSIX-only solution as CREATE_WAITABLE_TIMER_HIGH_RESOLUTION would be a Windows-only solution.

NostraMagister commented 6 months ago

Indeed the POSIX support on Windows is not natively complete, hence the alternative that I mentioned of using your suggestion using CREATE_WAITABLE_TIMER_HIGH_RESOLUTION on Windows (CreateWaitableTimerHighResolution) in combination with POSIX on Linux/Unix flavors and some pre-compile statements as a last resort solution.

However, there are POSIX libraries available for Windows that support the time functions. One such library is the open source POSIX Threads for Windows (pthreads-win32) library (https://sourceforge.net/projects/pthreads4w/). This library provides an implementation of the POSIX threads API for Windows, including support for the timer_create and timer_settime functions.

Asio high resolution timer code would have to tap into that source (because I assume it will not yield on the fact that Asio has no dependencies, which is IMO a key property) and extract the timer related code and make it Asio async.

The current normal usage example in Windows of this library (if one is prepared to have a dependency on a .lib or .dll file), is added below, to demonstrate that the underlying structure would perfectly allow Asio to make it fully asyn because this higher level code could already be made async. An implementation in Asio would mean one doesn't need the header and related .lib/.dll files anymore on Windows. In Linux POSIX is native and no extra library is needed.

My suggestion to explore such path is because CreateWaitableTimerHighResolution() will always be very proprietary and coding against the C++ Standard should not need anything additional, such as supporting a flad of an underlying propriatary layer (the CREATE_WAITABLE_TIMER_HIGH_RESOLUTION flag), besides declaring the time variable as high resolution clock, the rest should follow automatically. In that way it Asio stays 100% compatible at that level.

I added some comments to this code related to Async usage.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <pthread.h>

void timer_handler(int signum) 
{
    printf("Timer expired\n");
}

void* timer_thread(void* arg) 
{
    timer_t timerid;
    struct sigevent sev;
    struct itimerspec its;
    long long freq_nanosecs;
    int sig;

    /* Establish handler for timer signal */
    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo = SIGUSR1;
    sev.sigev_value.sival_ptr = &timerid;
    signal(SIGUSR1, timer_handler);

    /* Create the timer */
    if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) 
    {
        perror("timer_create");
        exit(EXIT_FAILURE);
    }

    /* Start the timer */
    freq_nanosecs = 1000000000;
    its.it_value.tv_sec = freq_nanosecs / 1000000000;
    its.it_value.tv_nsec = freq_nanosecs % 1000000000;
    its.it_interval.tv_sec = its.it_value.tv_sec;
    its.it_interval.tv_nsec = its.it_value.tv_nsec;

    if (timer_settime(timerid, 0, &its, NULL) == -1) 
   {
        perror("timer_settime");
        exit(EXIT_FAILURE);
    }

    /* Wait for the timer to expire */
    while (1) 
    {
        sigwait(&sig, &sig);
        if (sig != SIGUSR1) { continue;  }

        // async support would go here 
        ... callbacks
        ... possibly re-arm
        ... or break;
        ... etc
    }

    // Here it could be decided if the thread is kept alive waiting for a signal and receive a new timer instead of cleaning up. In that case there would be an extra outer loop of course. 

    /* Clean up */
    if (timer_delete(timerid) == -1) 
    {
        perror("timer_delete");
        exit(EXIT_FAILURE);
    }

    return NULL;
}

int main() 
{
    pthread_t tid;
    pthread_create(&tid, NULL, timer_thread, NULL);
    pthread_join(tid, NULL);
    return 0;
}
mtnpke commented 6 months ago

AFAICT, the linked SourceForge project does not have an implementation of the timer* API. Can you quote a source/the source code of that implementation? Did you compile and run the quoted example successfully on Windows with MSVC?

In addition, asio of course already uses the Windows waitable timer API for all timers on Windows today. So the change that is necessary would only be about adding support for the special high resolution flag, as opposed to the complete rewrite of all asio event handling to use a POSIX shim instead of WinAPI that you are proposing, if I understand it correctly. It also needs to interact correctly with all I/O operations that use the WinAPI, so changing the "backbone" to POSIX implies a lot more changes than just to the timers.

coding against the C++ Standard should not need anything additional, such as supporting a flad of an underlying propriatary layer (the CREATE_WAITABLE_TIMER_HIGH_RESOLUTION flag

The C++ standard is not POSIX. From the standard point of view, POSIX is just as much an external layer as is WinAPI. It is just not possible (at the moment) to create asynchronous event loops with timers using just standard C++ alone without any OS-specific API.

NostraMagister commented 6 months ago

Hmmm, on conformance https://sourceware.org/pthreads-win32/conformance.html the time functions are under a header 'the following functions have not been implemented'. I must have missed that. Hence the top level functions aren't in the library.

Microsoft removed http://www.microsoft.com/downloads/details.aspx?FamilyID=896C9688-601B-44F1-81A4-02878FF11778&displaylang=en which was the link to their version of pthreads-win32.

I figure that means back to the #ifdef solution.

No, I have compiled nothing yet (MSVC/GCC). I found the C++ Standard link that I mentioned in my first comment about high resolution timers recently and discovered at that point why my high resolution timers where never more precise than 15-25ms.

No sure why you understood that I thing that the C++ Standard library uses POSIX, I don't.

On the other hand your comment about POSIX on Windows being a much bigger effort is absolutely correct. And given it a good thought, if the support of high resolution timers is native/proprietary in an OS then it isn't really a dependency because one can assume the OS carries its own libraries. So, after all the #ifdef approach is probably the best way to go.

mtnpke commented 6 months ago

It doesn't really need #ifdefs since the Windows code is in entirely separate files anyway, see e.g. https://github.com/chriskohlhoff/asio/blob/ed5db1b50136bace796062c1a6eab0df9a74f8fa/asio/include/asio/detail/impl/win_iocp_io_context.ipp#L558

NostraMagister commented 6 months ago

I never looked in detail in the Asio implementation itself. Do you know if there are concrete plans to add high resolution timers/clock to the library? I see your OP #1328 is from Jul 2023 but didn't find any other posts indicating that something would be done with it.

I find your request a legit one given the growing importance of multi-media applications.

mtnpke commented 6 months ago

I do not have more information than what is written in this ticket, unfortunately.

NostraMagister commented 6 months ago

I have written a new post https://github.com/chriskohlhoff/asio/issues/1404 to request attention to the high resolution timers and asked if the makers of the library could take a position to the subject. I placed it in a Cross Platform Context.

I also asked if other poster would like to take a position to the suggested integration of high-res timers in Asio.