diff --git a/src/trio/_abc.py b/src/trio/_abc.py index abb682438..aa3fe84c9 100644 --- a/src/trio/_abc.py +++ b/src/trio/_abc.py @@ -42,7 +42,7 @@ def current_time(self) -> float: """ @abstractmethod - def deadline_to_sleep_time(self, deadline: float) -> float: + def deadline_to_sleep_time(self, timeout: float) -> float: """Compute the real time until the given deadline. This is called before we enter a system-specific wait function like @@ -65,6 +65,9 @@ def deadline_to_sleep_time(self, deadline: float) -> float: """ + def propagate(self, real_time_passed: float, virtual_timeout: float) -> None: + pass + class Instrument(ABC): # noqa: B024 # conceptually is ABC """The interface for run loop instrumentation. diff --git a/src/trio/_core/_mock_clock.py b/src/trio/_core/_mock_clock.py index e437464b6..1c32fe162 100644 --- a/src/trio/_core/_mock_clock.py +++ b/src/trio/_core/_mock_clock.py @@ -1,7 +1,6 @@ import time from math import inf -from .. import _core from .._abc import Clock from .._util import final from ._run import GLOBAL_RUN_CONTEXT @@ -105,12 +104,6 @@ def autojump_threshold(self, new_autojump_threshold: float) -> None: self._autojump_threshold = float(new_autojump_threshold) self._try_resync_autojump_threshold() - # runner.clock_autojump_threshold is an internal API that isn't easily - # usable by custom third-party Clock objects. If you need access to this - # functionality, let us know, and we'll figure out how to make a public - # API. Discussion: - # - # https://github.com/python-trio/trio/issues/1587 def _try_resync_autojump_threshold(self) -> None: try: runner = GLOBAL_RUN_CONTEXT.runner @@ -118,17 +111,12 @@ def _try_resync_autojump_threshold(self) -> None: runner.force_guest_tick_asap() except AttributeError: pass - else: - if runner.clock is self: - runner.clock_autojump_threshold = self._autojump_threshold - # Invoked by the run loop when runner.clock_autojump_threshold is - # exceeded. - def _autojump(self) -> None: - statistics = _core.current_statistics() - jump = statistics.seconds_to_next_deadline - if 0 < jump < inf: - self.jump(jump) + def propagate(self, real_time_passed: float, virtual_timeout: float) -> None: + if self._rate > 0: + self.jump(real_time_passed * self._rate) + else: + self.jump(virtual_timeout) def _real_to_virtual(self, real: float) -> float: real_offset = real - self._real_base @@ -136,19 +124,25 @@ def _real_to_virtual(self, real: float) -> float: return self._virtual_base + virtual_offset def start_clock(self) -> None: - self._try_resync_autojump_threshold() + pass def current_time(self) -> float: return self._real_to_virtual(self._real_clock()) - def deadline_to_sleep_time(self, deadline: float) -> float: - virtual_timeout = deadline - self.current_time() - if virtual_timeout <= 0: - return 0 - elif self._rate > 0: - return virtual_timeout / self._rate - else: - return 999999999 + def deadline_to_sleep_time(self, timeout: float) -> float: + virtual_timeout = max(0.0, timeout) + + real_timeout = virtual_timeout + + if self._rate > 0: + real_timeout /= self._rate + elif real_timeout > 0 and self._rate == 0: + real_timeout = 999999999.0 + + if real_timeout > self.autojump_threshold: + real_timeout = self.autojump_threshold + + return real_timeout def jump(self, seconds: float) -> None: """Manually advance the clock by the given number of seconds. diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index 8a7ddc1dc..16892608f 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -204,8 +204,8 @@ def start_clock(self) -> None: def current_time(self) -> float: return self.offset + perf_counter() - def deadline_to_sleep_time(self, deadline: float) -> float: - return deadline - self.current_time() + def deadline_to_sleep_time(self, timeout: float) -> float: + return timeout class IdlePrimedTypes(enum.Enum): @@ -1816,9 +1816,6 @@ class Runner: # type: ignore[explicit-any] trio_token: TrioToken | None = None asyncgens: AsyncGenerators = attrs.Factory(AsyncGenerators) - # If everything goes idle for this long, we call clock._autojump() - clock_autojump_threshold: float = inf - # Guest mode stuff is_guest: bool = False guest_tick_scheduled: bool = False @@ -2742,36 +2739,38 @@ def unrolled_run( # You know how people talk about "event loops"? This 'while' loop right # here is our event loop: while runner.tasks: + now = runner.clock.current_time() + if runner.runq: - timeout: float = 0 + virtual_timeout: float = 0 else: deadline = runner.deadlines.next_deadline() - timeout = runner.clock.deadline_to_sleep_time(deadline) - timeout = min(max(0, timeout), _MAX_TIMEOUT) + virtual_timeout = max(0, deadline - now) idle_primed = None if runner.waiting_for_idle: cushion, _ = runner.waiting_for_idle.keys()[0] - if cushion < timeout: - timeout = cushion + if cushion < virtual_timeout: + virtual_timeout = cushion idle_primed = IdlePrimedTypes.WAITING_FOR_IDLE - # We use 'elif' here because if there are tasks in - # wait_all_tasks_blocked, then those tasks will wake up without - # jumping the clock, so we don't need to autojump. - elif runner.clock_autojump_threshold < timeout: - timeout = runner.clock_autojump_threshold - idle_primed = IdlePrimedTypes.AUTOJUMP_CLOCK + + virtual_timeout = min(max(0, virtual_timeout), _MAX_TIMEOUT) + real_timeout = runner.clock.deadline_to_sleep_time(virtual_timeout) if "before_io_wait" in runner.instruments: - runner.instruments.call("before_io_wait", timeout) + runner.instruments.call("before_io_wait", real_timeout) # Driver will call io_manager.get_events(timeout) and pass it back # in through the yield - events = yield timeout + events = yield real_timeout + + new_now = runner.clock.current_time() + runner.clock.propagate(new_now - now, virtual_timeout) + runner.io_manager.process_events(events) if "after_io_wait" in runner.instruments: - runner.instruments.call("after_io_wait", timeout) + runner.instruments.call("after_io_wait", real_timeout) # Process cancellations due to deadline expiry now = runner.clock.current_time() @@ -2808,10 +2807,6 @@ def unrolled_run( runner.reschedule(task) else: break - else: - assert idle_primed is IdlePrimedTypes.AUTOJUMP_CLOCK - assert isinstance(runner.clock, _core.MockClock) - runner.clock._autojump() # Process all runnable tasks, but only the ones that are already # runnable now. Anything that becomes runnable during this cycle diff --git a/src/trio/_core/_tests/test_mock_clock.py b/src/trio/_core/_tests/test_mock_clock.py index 6131e7ba3..3a6e5a2ee 100644 --- a/src/trio/_core/_tests/test_mock_clock.py +++ b/src/trio/_core/_tests/test_mock_clock.py @@ -24,9 +24,9 @@ def test_mock_clock() -> None: with pytest.raises(ValueError, match=r"^time can't go backwards$"): c.jump(-1) assert c.current_time() == 1.2 - assert c.deadline_to_sleep_time(1.1) == 0 - assert c.deadline_to_sleep_time(1.2) == 0 - assert c.deadline_to_sleep_time(1.3) > 999999 + assert c.deadline_to_sleep_time(-0.1) == 0 + assert c.deadline_to_sleep_time(0.0) == 0 + assert c.deadline_to_sleep_time(0.1) == 999999999 with pytest.raises(ValueError, match=r"^rate must be >= 0$"): c.rate = -1 @@ -36,15 +36,15 @@ def test_mock_clock() -> None: assert c.current_time() == 1.2 REAL_NOW += 1 assert c.current_time() == 3.2 - assert c.deadline_to_sleep_time(3.1) == 0 - assert c.deadline_to_sleep_time(3.2) == 0 - assert c.deadline_to_sleep_time(4.2) == 0.5 + assert c.deadline_to_sleep_time(-0.1) == 0 + assert c.deadline_to_sleep_time(0.0) == 0 + assert c.deadline_to_sleep_time(1.0) == 0.5 c.rate = 0.5 assert c.current_time() == 3.2 - assert c.deadline_to_sleep_time(3.1) == 0 - assert c.deadline_to_sleep_time(3.2) == 0 - assert c.deadline_to_sleep_time(4.2) == 2.0 + assert c.deadline_to_sleep_time(-0.1) == 0 + assert c.deadline_to_sleep_time(0.0) == 0 + assert c.deadline_to_sleep_time(1.0) == 2.0 c.jump(0.8) assert c.current_time() == 4.0 @@ -80,11 +80,12 @@ async def test_mock_clock_autojump(mock_clock: MockClock) -> None: t = _core.current_time() # this should wake up before the autojump threshold triggers, so time # shouldn't change + # TODO: technically, this waits for an infinite virtual time - how should `current_time()` change? await wait_all_tasks_blocked() assert t == _core.current_time() # this should too await wait_all_tasks_blocked(0.01) - assert t == _core.current_time() + assert t + 0.01 == _core.current_time() # set up a situation where the autojump task is blocked for a long long # time, to make sure that cancel-and-adjust-threshold logic is working @@ -179,15 +180,16 @@ async def waiter() -> None: async def test_initialization_doesnt_mutate_runner() -> None: + # TODO: is this test even necessary now? before = ( GLOBAL_RUN_CONTEXT.runner.clock, - GLOBAL_RUN_CONTEXT.runner.clock_autojump_threshold, + # GLOBAL_RUN_CONTEXT.runner.clock.autojump_threshold, ) MockClock(autojump_threshold=2, rate=3) after = ( GLOBAL_RUN_CONTEXT.runner.clock, - GLOBAL_RUN_CONTEXT.runner.clock_autojump_threshold, + # GLOBAL_RUN_CONTEXT.runner.clock.autojump_threshold, ) assert before == after