From 8566820058399a5b0729a2491dbeae6dc5153319 Mon Sep 17 00:00:00 2001 From: Frank Yellin Date: Thu, 12 Dec 2024 16:40:24 -0800 Subject: [PATCH 1/5] Bring sleep back to WgpuAwaitable. Slowly back off on the amount of time sleeping. --- wgpu/backends/wgpu_native/_helpers.py | 47 +++++++++++++++++++-------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/wgpu/backends/wgpu_native/_helpers.py b/wgpu/backends/wgpu_native/_helpers.py index 6a77fc32..23283e44 100644 --- a/wgpu/backends/wgpu_native/_helpers.py +++ b/wgpu/backends/wgpu_native/_helpers.py @@ -3,8 +3,10 @@ import ctypes import sys import time +from collections.abc import Generator from queue import deque +import anyio import sniffio from ._ffi import ffi, lib, lib_path @@ -247,22 +249,21 @@ def __init__(self, title, callback, finalizer, poll_function=None): self.callback = callback # only used to prevent it from being gc'd self.finalizer = finalizer # function to finish the result self.poll_function = poll_function # call this to poll wgpu + self.event = None # only used in asynchronous cases self.result = None def set_result(self, result): self.result = (result, None) + if self.event: + self.event.set() def set_error(self, error): self.result = (None, error) - - def _is_done(self): - self.poll_function() - return self.result is not None + if self.event: + self.event.set() def _finish(self): try: - if not self.result: - raise RuntimeError(f"Waiting for {self.title} timed out.") result, error = self.result if error: raise RuntimeError(error) @@ -278,28 +279,46 @@ def sync_wait(self): elif not self.poll_function: raise RuntimeError("Expected callback to have already happened") else: - while not self._is_done(): - time.sleep(0) + backoff_time_generator = self._get_backoff_time_generator() + while True: + self.poll_function() + if self.result is not None: + break + time.sleep(next(backoff_time_generator)) + # We check the result after sleeping just in case another thread + # causes the callback to happen + if self.result is not None: + break + return self._finish() def __await__(self): # There is no documentation on what __await__() is supposed to return, but we # can certainly copy from a function that *does* know what to return async def wait_for_callback(): - # In all the async cases that I've tried, the result is either already set, or - # resolves after the first call to the poll function. To make sure that our - # sleep-logic actually works, we always do at least one sleep call. - await async_sleep(0) + # Set self.event before checking self.result to prevent data race. + self.event = anyio.Event() if self.result is not None: return if not self.poll_function: raise RuntimeError("Expected callback to have already happened") - while not self._is_done(): - await async_sleep(0) + sleep_generator = self._get_backoff_time_generator() + while not self.event.is_set(): + self.poll_function() + with anyio.move_on_after(next(sleep_generator)): + await self.event.wait() yield from wait_for_callback().__await__() return self._finish() + def _get_backoff_time_generator(self) -> Generator[float, None, None]: + for _ in range(5): + yield 0 + for i in range(1, 20): + yield i / 2000.0 # ramp up from 0ms to 10ms + while True: + yield 0.01 + class ErrorHandler: """Object that logs errors, with the option to collect incoming From e9242324c362eb1c8486af62b1d661c2e3446611 Mon Sep 17 00:00:00 2001 From: Frank Yellin Date: Thu, 12 Dec 2024 16:55:27 -0800 Subject: [PATCH 2/5] Need anyio --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 447d64c5..da122d44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "cffi>=1.15.0", "rubicon-objc>=0.4.1; sys_platform == 'darwin'", "sniffio", + "anyio>=4.7.0" ] [project.optional-dependencies] From 6d896da10a6285a20133c4c78718abcde64334d7 Mon Sep 17 00:00:00 2001 From: Frank Yellin Date: Thu, 19 Dec 2024 15:37:39 -0800 Subject: [PATCH 3/5] Need anyio --- wgpu/backends/wgpu_native/_helpers.py | 37 ++++++++++++++------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/wgpu/backends/wgpu_native/_helpers.py b/wgpu/backends/wgpu_native/_helpers.py index 23283e44..bbe986ab 100644 --- a/wgpu/backends/wgpu_native/_helpers.py +++ b/wgpu/backends/wgpu_native/_helpers.py @@ -6,7 +6,6 @@ from collections.abc import Generator from queue import deque -import anyio import sniffio from ._ffi import ffi, lib, lib_path @@ -249,18 +248,13 @@ def __init__(self, title, callback, finalizer, poll_function=None): self.callback = callback # only used to prevent it from being gc'd self.finalizer = finalizer # function to finish the result self.poll_function = poll_function # call this to poll wgpu - self.event = None # only used in asynchronous cases self.result = None def set_result(self, result): self.result = (result, None) - if self.event: - self.event.set() def set_error(self, error): self.result = (None, error) - if self.event: - self.event.set() def _finish(self): try: @@ -294,22 +288,29 @@ def sync_wait(self): def __await__(self): # There is no documentation on what __await__() is supposed to return, but we - # can certainly copy from a function that *does* know what to return + # can certainly copy from a function that *does* know what to return. + # It would also be nice if wait_for_callback and sync_wait() could be merged, + # but Python has no wait of combining them. async def wait_for_callback(): # Set self.event before checking self.result to prevent data race. - self.event = anyio.Event() if self.result is not None: - return - if not self.poll_function: + pass + elif not self.poll_function: raise RuntimeError("Expected callback to have already happened") - sleep_generator = self._get_backoff_time_generator() - while not self.event.is_set(): - self.poll_function() - with anyio.move_on_after(next(sleep_generator)): - await self.event.wait() - - yield from wait_for_callback().__await__() - return self._finish() + else: + backoff_time_generator = self._get_backoff_time_generator() + while True: + self.poll_function() + if self.result is not None: + break + await async_sleep(next(backoff_time_generator)) + # We check the result after sleeping just in case another + # flow of control causes the callback to happen + if self.result is not None: + break + return self._finish() + + return (yield from wait_for_callback().__await__()) def _get_backoff_time_generator(self) -> Generator[float, None, None]: for _ in range(5): From dab3c20827cede175ee6996e288b0724139e7ece Mon Sep 17 00:00:00 2001 From: Frank Yellin Date: Fri, 20 Dec 2024 12:43:37 -0800 Subject: [PATCH 4/5] Need anyio --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index da122d44..447d64c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "cffi>=1.15.0", "rubicon-objc>=0.4.1; sys_platform == 'darwin'", "sniffio", - "anyio>=4.7.0" ] [project.optional-dependencies] From 7fae0878aa119efa1b32644fa7dedca311ef3670 Mon Sep 17 00:00:00 2001 From: Frank Yellin Date: Wed, 25 Dec 2024 11:17:30 -0800 Subject: [PATCH 5/5] Remove no-longer-needed-comment --- wgpu/backends/wgpu_native/_helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/wgpu/backends/wgpu_native/_helpers.py b/wgpu/backends/wgpu_native/_helpers.py index bbe986ab..5e3f05ef 100644 --- a/wgpu/backends/wgpu_native/_helpers.py +++ b/wgpu/backends/wgpu_native/_helpers.py @@ -292,7 +292,6 @@ def __await__(self): # It would also be nice if wait_for_callback and sync_wait() could be merged, # but Python has no wait of combining them. async def wait_for_callback(): - # Set self.event before checking self.result to prevent data race. if self.result is not None: pass elif not self.poll_function: