anyio.sleep() timing inconsistencies at small sleep times between asyncio and trio

337 views Asked by At

python 3.9.5 (anyio, asyncio, trio) windows 10

I am attempting to use anyio.sleep() to limit how fast my main app loop is running, in addition to having another async loop in a class running as a task. I've noticed when I decrease the sleep period (increase the refresh rate), asyncio seems to report incorrect, very short sleep times (high refresh rate) while trio seems to be reporting more expected values.

additionally, when the start function loop period is lower than the main loop refresh rate, asyncio seems to give unpredictable results. at higher than 40 hz, asyncio seems to refresh faster than intended, getting exponentially worse when inputting high refresh rates.

in the code sample below, FPSCounter is just a helper class. the whole block is copy-and-paste runnable.

here are some testing results with varying refresh rates by using the code at the bottom.

********** start hz: 33.33 / main hz 60.0 **********
============= trio =============
start function refresh rate (hz)
avg: 32.111337719289175
max: 32.20671951236294
min: 32.01595592621541
main function refresh rate (hz)
avg: 57.98613394545777
max: 58.00646191985787
min: 57.96580597105768
============= asyncio =============
start function refresh rate (hz)
avg: 58.18328943732051
max: 58.22729607295037
min: 58.13928280169065
main function refresh rate (hz)
avg: 58.184786967810574
max: 58.22527199837126
min: 58.144301937249885
********** start hz: 40.0 / main hz 240.0 **********
============= trio =============
start function refresh rate (hz)
avg: 39.234583680312994
max: 39.38072626526089
min: 39.0884410953651
main function refresh rate (hz)
avg: 215.89497360297648
max: 216.52241650591293
min: 215.26753070004003
============= asyncio =============
start function refresh rate (hz)
avg: 64.0348432659224
max: 64.04419798485424
min: 64.02548854699056
main function refresh rate (hz)
avg: 191.6032335310934
max: 192.06711593299164
min: 191.13935112919512
********** start hz: 80.0 / main hz 240.0 **********
============= trio =============
start function refresh rate (hz)
avg: 74.98626248669126
max: 75.31187257068325
min: 74.66065240269926
main function refresh rate (hz)
avg: 220.92863862387009
max: 222.9905452008835
min: 218.86673204685667
============= asyncio =============
start function refresh rate (hz)
avg: 461.9392486786134
max: 732.8401458173149
min: 191.0383515399119
main function refresh rate (hz)
avg: 460.52822045374194
max: 730.0229804121695
min: 191.0334604953143
import anyio

from time import perf_counter_ns
from collections import deque

class FPSCounter:
    def __init__(self):
        self.current_fps = 0
        self.previous_fps = 0
        self.last_time = 0
        self.update_interval = 1 #sec
        self.frame_times = deque()
        self.intervals = []
        self.history = []
        
    def new_frame(self):
        self.frame_times.append(perf_counter_ns())    
    
    def update_fps(self):
        if (len(self.frame_times) < 2):
            if(len(self.frame_times) == 0 or 
               (perf_counter_ns() > (self.frame_times.pop() + 2*1_000_000_000*self.update_interval))):
               self.previous_fps = self.current_fps
               self.current_fps = 0
        else:
            if (self.last_time == 0):
                self.last_time = self.frame_times.popleft()
                
            while(len(self.frame_times) > 0):
                t = self.frame_times.popleft()
                if t > self.last_time:
                    self.intervals.append(t-self.last_time)
                    self.last_time = t
                
            if len(self.intervals) == 0: return
            
            avg = sum(self.intervals) / len(self.intervals)
            self.previous_fps = self.current_fps
            self.current_fps = 1_000_000_000.0 / float(avg)
            
            self.intervals.clear()
            self.history.append(self.current_fps)

async def start(fps: float, counter: FPSCounter, duration):
    last_time = anyio.current_time()
    with anyio.move_on_after(duration) as scope:
        while True:
            counter.new_frame()
            last_time = anyio.current_time()
            # group of tasks to do this loop
            async with anyio.create_task_group() as tg:
                # sleep to cause refresh rate
                await anyio.sleep(1.0/fps - anyio.current_time() + last_time)
            # do stuff
 
async def main(start_loop_hz, main_loop_hz):
    fps_start = FPSCounter()
    fps_main = FPSCounter()
    
    last_fps_update = anyio.current_time()
    start_time = anyio.current_time()
    last_time = anyio.current_time()
    duration = 3.0
    # main group to start background 'start' coro
    async with anyio.create_task_group() as maintg:
        maintg.start_soon(start, start_hz, fps_start, duration)
        # main app loop
        with anyio.move_on_after(duration) as scope:
            while anyio.current_time() < (start_time+duration):
                fps_main.new_frame()
                last_time = anyio.current_time()
                if (anyio.current_time() - last_fps_update) > 1.0:
                    last_fps_update = anyio.current_time()
                    fps_main.update_fps()
                    fps_start.update_fps()
                # sleep to cause the refresh rate for main loop
                await anyio.sleep(1.0/(main_hz) - anyio.current_time() + last_time)
                # do stuff in main app loop
            
        fps_start_avg = sum(fps_start.history) / len(fps_start.history)
        fps_start_max = max(fps_start.history)
        fps_start_min = min(fps_start.history)
        print(f'start function refresh rate (hz)\navg: {fps_start_avg}\nmax: {fps_start_max}\nmin: {fps_start_min}')
        
        fps_main_avg = sum(fps_main.history) / len(fps_main.history)
        fps_main_max = max(fps_main.history)
        fps_main_min = min(fps_main.history)
        print(f'main function refresh rate (hz)\navg: {fps_main_avg}\nmax: {fps_main_max}\nmin: {fps_main_min}')
        # exit()
        
if __name__ == '__main__':
    start_hz = 33.33 # refresh rate for the start function loop
    main_hz = 60.0 # refresh rate for the main loop 
    print(f'********** start hz: {start_hz} / main hz {main_hz} **********')
    print('============= trio =============')
    anyio.run(main, start_hz, main_hz, backend='trio')
    print('============= asyncio =============')
    anyio.run(main, start_hz, main_hz, backend='asyncio')
0

There are 0 answers