Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/trio/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
46 changes: 20 additions & 26 deletions src/trio/_core/_mock_clock.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -105,50 +104,45 @@ 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
if runner.is_guest:
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
virtual_offset = self._rate * real_offset
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.
Expand Down
41 changes: 18 additions & 23 deletions src/trio/_core/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
26 changes: 14 additions & 12 deletions src/trio/_core/_tests/test_mock_clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading