From 1f77a91a9c2ac41e1174d8516c314b3ff5f34d64 Mon Sep 17 00:00:00 2001 From: Casey Howard Date: Mon, 16 Oct 2023 13:50:25 -0400 Subject: [PATCH] feat(uvloop): Support event loop utilization queries --- tests/test_base.py | 15 +++++++++++++++ uvloop/includes/uv.pxd | 9 +++++++++ uvloop/loop.pxd | 1 + uvloop/loop.pyi | 1 + uvloop/loop.pyx | 27 +++++++++++++++++++++++++++ 5 files changed, 53 insertions(+) diff --git a/tests/test_base.py b/tests/test_base.py index a8eb3b4d..5dc65512 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -737,6 +737,21 @@ def scheduler(): class TestBaseUV(_TestBase, UVTestCase): + def test_event_loop_utilization(self): + self.assertTrue(self.loop.event_loop_utilization() == (0., 0., 0.)) + + async def run(): + await asyncio.sleep(0.2) + time.sleep(0.05) + return self.loop.event_loop_utilization() + + i, a, p = self.loop.run_until_complete(run()) + self.assertTrue(100 < i < 400) + self.assertTrue(a > 0.) + self.assertTrue(0. < p < 1.) + + self.assertTrue(self.loop.event_loop_utilization() == (0., 0., 0.)) + def test_loop_create_future(self): fut = self.loop.create_future() self.assertTrue(isinstance(fut, asyncio.Future)) diff --git a/uvloop/includes/uv.pxd b/uvloop/includes/uv.pxd index 87651306..d09bb41e 100644 --- a/uvloop/includes/uv.pxd +++ b/uvloop/includes/uv.pxd @@ -202,6 +202,10 @@ cdef extern from "uv.h" nogil: UV_REQ_TYPE_PRIVATE, UV_REQ_TYPE_MAX + ctypedef enum uv_loop_option: + UV_LOOP_BLOCK_SIGNAL = 0, + UV_METRICS_IDLE_TIME + ctypedef enum uv_run_mode: UV_RUN_DEFAULT = 0, UV_RUN_ONCE, @@ -281,6 +285,7 @@ cdef extern from "uv.h" nogil: int uv_loop_init(uv_loop_t* loop) int uv_loop_close(uv_loop_t* loop) int uv_loop_alive(uv_loop_t* loop) + int uv_loop_configure(uv_loop_t* loop, uv_loop_option option, ...) int uv_loop_fork(uv_loop_t* loop) int uv_backend_fd(uv_loop_t* loop) @@ -458,6 +463,10 @@ cdef extern from "uv.h" nogil: uv_calloc_func calloc_func, uv_free_func free_func) + # Metrics + + uint64_t uv_metrics_idle_time(uv_loop_t* loop) + # Process ctypedef void (*uv_exit_cb)(uv_process_t*, int64_t exit_status, diff --git a/uvloop/loop.pxd b/uvloop/loop.pxd index 56134733..a941aa4b 100644 --- a/uvloop/loop.pxd +++ b/uvloop/loop.pxd @@ -138,6 +138,7 @@ cdef class Loop: cdef _close(self) cdef _stop(self, exc) + cdef uint64_t _event_loop_idle_time(self) cdef uint64_t _time(self) cdef inline _queue_write(self, UVStream stream) diff --git a/uvloop/loop.pyi b/uvloop/loop.pyi index 9c8c4623..d0151d3d 100644 --- a/uvloop/loop.pyi +++ b/uvloop/loop.pyi @@ -37,6 +37,7 @@ class Loop: def call_at( self, when: float, callback: Callable[..., Any], *args: Any, context: Optional[Any] = ... ) -> asyncio.TimerHandle: ... + def _event_loop_utilization(self) -> Tuple[float, float, float]: ... def time(self) -> float: ... def stop(self) -> None: ... def run_forever(self) -> None: ... diff --git a/uvloop/loop.pyx b/uvloop/loop.pyx index b2ae509b..1db91518 100644 --- a/uvloop/loop.pyx +++ b/uvloop/loop.pyx @@ -162,6 +162,10 @@ cdef class Loop: self._recv_buffer_in_use = 0 err = uv.uv_loop_init(self.uvloop) + if err < 0: + raise convert_error(err) + + err = uv.uv_loop_configure(self.uvloop, uv.UV_METRICS_IDLE_TIME) if err < 0: raise convert_error(err) self.uvloop.data = self @@ -527,6 +531,7 @@ cdef class Loop: self._thread_id = PyThread_get_thread_ident() self._running = 1 + self._loop_start_time = self._time() self.handler_check__exec_writes.start() self.handler_idle.start() @@ -628,6 +633,10 @@ cdef class Loop: self._default_executor = None executor.shutdown(wait=False) + cdef uint64_t _event_loop_idle_time(self): + """Returns number of nanoseconds the loop has been idle""" + return uv.uv_metrics_idle_time(self.uvloop) + cdef uint64_t _time(self): # asyncio doesn't have a time cache, neither should uvloop. uv.uv_update_time(self.uvloop) # void @@ -1337,6 +1346,24 @@ cdef class Loop: return self.call_later( when - self.time(), callback, *args, context=context) + def _event_loop_utilization(self): + """Returns idle and active time in milliseconds and the percentage of + time the event loop is active + """ + + idle = 0. + active = 0. + utilization = 0. + + if not self._running: + return idle, active, utilization + + idle = self._event_loop_idle_time() / 10 ** 6 + active = self._time() - self._loop_start_time - idle + utilization = active / (active + idle) + + return idle, active, utilization + def time(self): """Return the time according to the event loop's clock.