Search code examples
pythonpython-asynciotwistedevent-loop

Highest fidelity I can get out of Twisted LoopingCallback is around 100ms (.1sec)


[Update] I'm using Twisted 22.4.0 and Python 3.9.6

I'm trying to write an asynchronous application that must run an event loop at 250Hz. So far, Twisted is simply not fast enough to work for my application (but I would like to know if it's possible to fix this). On a Windows 10 i5 laptop, the highest frequency I can achieve in a LoopingCall is around 50hz. When I adjust the following code runs ok at 50hz and successfully prints out "took 1.002 sec", but at 100Hz, the code takes typically 1.5seconds to run, and I need my code to be able to run at .004 (250Hz).

from twisted.internet.task import LoopingCall
from twisted.internet import reactor
import time

class Loop():
    def __init__(self, hz):
        self.hz = hz
        self.lc = LoopingCall(self.fast_task)
        self.num_calls = 0
        self.lc.start(1/hz)
        reactor.run() # **Forgot to add this the first time**

    def fast_task(self):
        if self.num_calls == 0:
            self.start_time = time.time()
        if self.num_calls == self.hz:
            print("Stopping reactor...")
            print(f"took: {time.time() - self.start_time} sec")
            reactor.stop()
            return
        self.num_calls += 1

if __name__ == "__main__":
    l = Loop(100)

The above code typically takes ~1.5s to run. My question: Is there any way to speed this event loop up in Twisted on Windows?

I've run some similar code in asyncio and asyncio can definitely handle a 250Hz loop on my laptop. So one of the next things I tried was using the asyncioreactor with Twisted. Turns out, it still takes the same amount of time as the above code which doesn't use asyncioreactor.

But, I like the simplicity of Twisted for my use case - I need a few TCP servers and clients and a few UDP servers and clients plus some other heavy I/O processing.

One other note, I did find this ticket (https://twistedmatrix.com/trac/ticket/2424) for Twisted in which I found out [Edit](that the author of Twisted - Glyph - chose not to move to a monotonic based time unless there was an API change, which to my knowledge hasn't been implemented except maybe when used with asyncioreactor?). This also gives me other concerns about using Twisted as a reliable, high frequency event loop, such as NTP clock adjustments. Now, it may be that using asyncio under the hood (with asyncioreactor) takes care of this problem, but it certainly doesn't seem to offer any speed advantage.

[Update 2] This may have fixed my problem: I adjusted the windows sleep resolution with the following code, and now my LoopingCall seems to run the above code reliably in 1 sec at 250Hz, and reliably up to 1000Hz:

from ctypes import windll
windll.winmm.timeBeginPeriod(1) # This sets the time sleep resolution to 1 ms

[Update 3]

I've included the code I used to create the loop with aysncioreactor.

Note: you'll notice that I'm using WindowsSelectorEventLoopPolicy() - this is due to not having the latest Visual C++ libraries installed (not sure if that's important info here, though)

Note 2: I'm new to twisted, so I could be using this incorrectly (the usage of asyncioreactor, or the actual LoopingCall - although the LoopingCall seems pretty straightforward)

Note 3: I'm running on Windows 10 v21H2, Processor: 1.6GHz i5

The v21H2 is important here since it's after v2004:

From: https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod

Prior to Windows 10, version 2004, this function affects a global Windows setting. For all processes Windows uses the lowest value (that is, highest resolution) requested by any process. Starting with Windows 10, version 2004, this function no longer affects global timer resolution. For processes which call this function, Windows uses the lowest value (that is, highest resolution) requested by any process. For processes which have not called this function, Windows does not guarantee a higher resolution than the default system resolution.

To see if I could prove this out, I've tried running Windows Media Player, Skype, and other programs while not calling timeBeginPeriod(1) (the thought being that another program by another process would set a lower resolution and that would affect my program. But this didn't change the timings you see below.

Note 4: Timings for a 3 second run (3 runs each) @ 1000Hz:

   asyncioreactor with timeBeginPeriod(1):    [3.019, 3.029, 3.009]
   asyncioreactor with no timeBeginPeriod(1): [42.859, 43.65, 43.152]
   no asyncioreactor with timeBeginPeriod(1): [3.012, 3.519, 3.146]
   no asyncioreactor, no  timeBeginPeriod(1): [45.247, 44.957, 45.325] 

My implementation using asyncioreactor

import asyncio
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

from twisted.internet import asyncioreactor
asyncioreactor.install()
from twisted.internet.task import LoopingCall

from twisted.internet import reactor
import time

from ctypes import windll
windll.winmm.timeBeginPeriod(1)

class Loop():
    def __init__(self, hz=1000):
        self.hz = hz
    ...
    ...

Solution

  • To fix my problem:

    I adjusted the windows sleep resolution with the following code, and now my LoopingCall seems to run the above code reliably in 1 sec at 250Hz, and reliably up to 1000Hz:

    from ctypes import windll
    windll.winmm.timeBeginPeriod(1) # This sets the time sleep resolution to 1 ms