From 783d334e78b48d21df44cdd7cddb635490dc2d8b Mon Sep 17 00:00:00 2001 From: Jaswant Panchumarti Date: Fri, 18 Sep 2026 12:41:03 -0400 Subject: [PATCH 1/2] fix(scheduler): sleep only for what is left of target_period_s --- src/trame_rca/schedulers/image_scheduler.py | 17 +++++++++++------ src/trame_rca/schedulers/video_scheduler.py | 5 ++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/trame_rca/schedulers/image_scheduler.py b/src/trame_rca/schedulers/image_scheduler.py index 95d59df..6a0ab12 100644 --- a/src/trame_rca/schedulers/image_scheduler.py +++ b/src/trame_rca/schedulers/image_scheduler.py @@ -3,6 +3,7 @@ import asyncio import os from asyncio import Event, Queue, sleep +from time import monotonic from concurrent.futures import Executor from concurrent.futures.thread import ThreadPoolExecutor from time import time_ns @@ -115,21 +116,25 @@ async def _render(self): interactive = self._interactive_quality still = self._still_quality - + started = monotonic() self._render_frame(interactive) if interactive == still: - await sleep(self._target_period_s) + # Sleep only for what is left of the _target_period_s. + await sleep(max(0.0, self._target_period_s - (monotonic() - started))) continue - if await self._wait_for_still(): + if await self._wait_for_still(started): self._render_frame(still) - async def _wait_for_still(self): + async def _wait_for_still(self, timer_started_at): self._request_event.clear() - for _ in range(self._n_period_until_still_render): - await sleep(self._target_period_s) + for i in range(1, self._n_period_until_still_render + 1): + # Sleep only for what is left of the i-th _target_period_s since the timer started, so that the time + # spent rendering the interactive frame (and any loop overhead) is not added on top of the wait. + deadline = timer_started_at + i * self._target_period_s + await sleep(max(0.0, deadline - monotonic())) if self._request_event.is_set(): return False diff --git a/src/trame_rca/schedulers/video_scheduler.py b/src/trame_rca/schedulers/video_scheduler.py index afd5900..2d23105 100644 --- a/src/trame_rca/schedulers/video_scheduler.py +++ b/src/trame_rca/schedulers/video_scheduler.py @@ -2,6 +2,7 @@ import asyncio from asyncio import Event, sleep +from time import monotonic from typing import TYPE_CHECKING, Callable from trame.app import asynchronous @@ -113,8 +114,10 @@ async def _render(self): break self._request_event.clear() + started = monotonic() self._rca_encoder.encode(self._rca.render_window) - await sleep(self._target_period_s) + # Sleep only for what is left of the _target_period_s. + await sleep(max(0.0, self._target_period_s - (monotonic() - started))) def _push(self, content: bytes, meta: dict, _m_time: int): if self._push_callback is not None: From 47a4b2dbd84b660f25266c18a15181306adb79e1 Mon Sep 17 00:00:00 2001 From: Jaswant Panchumarti Date: Fri, 18 Sep 2026 12:49:27 -0400 Subject: [PATCH 2/2] chore(profile): profile different stages of video encoder --- src/trame_rca/encoders/video_encoder.py | 50 +++++++++++++++---------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/trame_rca/encoders/video_encoder.py b/src/trame_rca/encoders/video_encoder.py index 7000cc6..ca06be6 100644 --- a/src/trame_rca/encoders/video_encoder.py +++ b/src/trame_rca/encoders/video_encoder.py @@ -2,6 +2,7 @@ from time import time_ns from typing import Callable, Optional +from trame_common.utils import profiler from vtk_streaming.vtkStreamingCore import ( VTKPF_IYUV, VTKVC_AV1, @@ -38,15 +39,17 @@ def available_codecs() -> list[dict]: """Codecs this server can encode, server-preferred first.""" + timer = profiler.Timer("rca.video.available_codecs") result = [] - for codec, name, probes in _CODECS: - if vtkEncoderFactory.CheckAvailability(codec): - result.append( - { - "codec": name, - "probes": probes, - } - ) + with timer: + for codec, name, probes in _CODECS: + if vtkEncoderFactory.CheckAvailability(codec): + result.append( + { + "codec": name, + "probes": probes, + } + ) return result @@ -113,6 +116,9 @@ def __init__( codecs: Optional[list[str]] = None, defer: bool = False, ) -> None: + self._timer_encoder = profiler.Timer("rca.video.encode") + self._timer_capture = profiler.Timer("rca.video.frame_capture") + self._timer_configure = profiler.Timer("rca.video.configure") self._render_window = render_window self._push_callback = push_callback self._codecs = codecs @@ -132,17 +138,19 @@ def codecs(self) -> Optional[list[str]]: def configure(self, codecs: Optional[list[str]]) -> dict: """(Re)create the encoder for the given codec ranking. Returns :func:`describe_encoder`.""" - self.release() - self._codecs = codecs - self.encoder = create_encoder(codecs) - if self.encoder is None: - raise RuntimeError( - "No suitable video encoder is available on this machine." + with self._timer_configure: + self.release() + self._codecs = codecs + self.encoder = create_encoder(codecs) + if self.encoder is None: + raise RuntimeError( + "No suitable video encoder is available on this machine." + ) + self.encoder.AddObserver( + vtkVideoEncoder.EncodedVideoChunkEvent, self._on_encoded_chunk ) - self.encoder.AddObserver( - vtkVideoEncoder.EncodedVideoChunkEvent, self._on_encoded_chunk - ) - self._initialize(self._render_window) + + self._initialize(self._render_window) return self.describe() def describe(self) -> dict: @@ -192,8 +200,10 @@ def encode(self, render_window: vtkRenderWindow): return if self._window_size != render_window.GetSize(): self._set_size(render_window_size=render_window.size) - self.frame.Capture(render_window) - self.encoder.Encode(self.frame) + with self._timer_capture: + self.frame.Capture(render_window) + with self._timer_encoder: + self.encoder.Encode(self.frame) def release(self): if self.encoder is not None: