-
Notifications
You must be signed in to change notification settings - Fork 8
Add loop.call_soon_threadsafe() and re-implement precise sleep
#146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Note that for the trio and wx loops, this re-introduces #107, so to some degree this is a regression. |
|
cc @Vipitis |
|
hm, I checked out the branch and it doesn't seem to be much different than the previous improvement we had. Still better compared to what we had originally - but also not perfect. unrestricted validation: I wanted to try some external benchmarking tools like PresentMon anyway to better understand the CPU vs GPU side - but haven't gotten there yet to see if it even works with wgpu. |
That's also not the point. The purpose is that |
|
This is ready. All backends now have a precise timer. Mostly through a new threaded callback util. Qt through PreciseTimer. Tested on PyQt5, PyQt6, PySide2, PySide6. Also see the updated top post. |
|
I see a lot of notes about preciseness on Windows, what about the other platforms? Do they just sidestep this whole thing? |
loop.call_soon_threadsafe() and re-implement precise sleep
Windows historically uses ticks that go at 64 ticks per second, i.e. 15.625 ms each. Other platforms are "tickless" and have microsecond resolution. edit: I'll add this as a comment somewhere. |
|
I also added The real use-case is polling wgpu. I first thought I wanted to do that using threads, but we did not have a backend-agnostic way to let a thread invoke a call in the main thread. So I thought to poll using |
|
I ❤️ this piece of code: class RawLoop(BaseLoop):
...
def _rc_run(self):
while not self._should_stop:
callback = self._queue.get(True, None)
try:
callback()
except Exception as err:
logger.error(f"Error in RawLoop callback: {err}")
def _rc_call_later(self, delay, callback):
call_later_from_thread(delay, self._rc_call_soon_threadsafe, callback)
def _rc_call_soon_threadsafe(self, callback):
self._queue.put(callback) |






Revisiting #109 (and #107)
Intro
In #109 I opted for a solution that did some
time.sleep()in the scheduler. This is a relatively simple solution, that fixed the problem of lower-than-max-fps framerates for all backends.This morning I was happily planning to implement the polling for wgpu, so that we can properly leverage async, but I bumped in this problem again. I decided to fix the problem in a per-backend manner, so that we can
await sleep()with precise timers everywhere (and for the wgpu-polling).Changes
loop.call_soon_threadsafe()._rc_call_soon_threadsafe()._coreutils.call_later_from_thread()rawloop is now dead-simple, because the util takes care of all the scheduling.await sleep(..), without any trickery.Some low-level details
Mostly to record design decisions. You can probably skip this.
Some more details about how polling led me here .... I thought about a few different approaches to do the polling:
loop.call_soon(), which means that thecall_soon()of all backends will have to be thread-safe, which is currently not the case.Some details about an earlier posted solution that uses a threadpool, see this link.
Some findings while I was experimenting
Mostly to record interesting findings. You can probably skip this.
I tried several things to try to find a mechanism that does not suffer from imprecise timer. BTW, ChatGTP is absolutely worthless for this topic.
Allow a thread to wait for a signal from another thread on a timeout
This is a simple mechanism that would have been useful. But it looks like its impossible. Using
time.sleep()is precise, but any method that has a timeout (threading.Event.wait(),Queue.get(),threading.Lock.acquire(), usingselecton a socket) is inprecise, and waits 15.6 ms on average.Let a sub-thread invoke a method in the main thread
This actually does work precise!
For Asyncio, you can take a
asyncio.Event, let a task wait for it, and then let the worker thread doloop.call_soon_threadsafe(event.set).For Qt it works with a litle code like this:
And then the worker thread simply does
InvokeMethod(self.cb).I considered an approach where
rendercanvaswould do the scheduling in a thread and then use this mechanism to make it do stuff in the main thread. But that would mean things need to be differen on systems where we have no threads, like Pyodide.Piece of code to check that it works