Skip to content

Commit 25289f1

Browse files
committed
Rework clock usage in the event loop
1 parent a6610fc commit 25289f1

File tree

5 files changed

+51
-59
lines changed

5 files changed

+51
-59
lines changed

src/trio/_abc.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def current_time(self) -> float:
4343
"""
4444

4545
@abstractmethod
46-
def deadline_to_sleep_time(self, deadline: float) -> float:
46+
def deadline_to_sleep_time(self, deadline: float) -> tuple[float, float]:
4747
"""Compute the real time until the given deadline.
4848
4949
This is called before we enter a system-specific wait function like
@@ -66,14 +66,8 @@ def deadline_to_sleep_time(self, deadline: float) -> float:
6666
6767
"""
6868

69-
@property
70-
def autojump_threshold(self) -> float:
71-
return inf
72-
73-
def autojump(self) -> None:
74-
# If `autojump_threshold()` has the default implementation (returning `inf`),
75-
# this will never be called.
76-
raise NotImplementedError
69+
def propagate(self, real_time_passed: float, virtual_timeout: float) -> None:
70+
pass
7771

7872

7973
class Instrument(ABC): # noqa: B024 # conceptually is ABC

src/trio/_core/_mock_clock.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,11 @@ def _try_resync_autojump_threshold(self) -> None:
113113
except AttributeError:
114114
pass
115115

116-
# Invoked by the run loop when runner.clock_autojump_threshold is
117-
# exceeded.
118-
def autojump(self) -> None:
119-
statistics = _core.current_statistics()
120-
jump = statistics.seconds_to_next_deadline
121-
if 0 < jump < inf:
122-
self.jump(jump)
116+
def propagate(self, real_time_passed: float, virtual_timeout: float) -> None:
117+
if self._rate > 0:
118+
self.jump(real_time_passed * self._rate)
119+
else:
120+
self.jump(virtual_timeout)
123121

124122
def _real_to_virtual(self, real: float) -> float:
125123
real_offset = real - self._real_base
@@ -132,14 +130,20 @@ def start_clock(self) -> None:
132130
def current_time(self) -> float:
133131
return self._real_to_virtual(self._real_clock())
134132

135-
def deadline_to_sleep_time(self, deadline: float) -> float:
136-
virtual_timeout = deadline - self.current_time()
137-
if virtual_timeout <= 0:
138-
return 0
139-
elif self._rate > 0:
140-
return virtual_timeout / self._rate
141-
else:
142-
return 999999999
133+
def deadline_to_sleep_time(self, timeout: float) -> tuple[float, float]:
134+
virtual_timeout = max(0.0, timeout)
135+
136+
real_timeout = virtual_timeout
137+
138+
if self._rate > 0:
139+
real_timeout /= self._rate
140+
elif real_timeout > 0 and self._rate == 0:
141+
real_timeout = 999999999.0
142+
143+
if real_timeout > self.autojump_threshold:
144+
real_timeout = self.autojump_threshold
145+
146+
return real_timeout, virtual_timeout
143147

144148
def jump(self, seconds: float) -> None:
145149
"""Manually advance the clock by the given number of seconds.

src/trio/_core/_run.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ def start_clock(self) -> None:
204204
def current_time(self) -> float:
205205
return self.offset + perf_counter()
206206

207-
def deadline_to_sleep_time(self, deadline: float) -> float:
208-
return deadline - self.current_time()
207+
def deadline_to_sleep_time(self, timeout: float) -> tuple[float, float]:
208+
return timeout, timeout
209209

210210

211211
class IdlePrimedTypes(enum.Enum):
@@ -2739,36 +2739,38 @@ def unrolled_run(
27392739
# You know how people talk about "event loops"? This 'while' loop right
27402740
# here is our event loop:
27412741
while runner.tasks:
2742+
now = runner.clock.current_time()
2743+
27422744
if runner.runq:
27432745
timeout: float = 0
27442746
else:
27452747
deadline = runner.deadlines.next_deadline()
2746-
timeout = runner.clock.deadline_to_sleep_time(deadline)
2747-
timeout = min(max(0, timeout), _MAX_TIMEOUT)
2748+
timeout = max(0, deadline - now)
27482749

27492750
idle_primed = None
27502751
if runner.waiting_for_idle:
27512752
cushion, _ = runner.waiting_for_idle.keys()[0]
27522753
if cushion < timeout:
27532754
timeout = cushion
27542755
idle_primed = IdlePrimedTypes.WAITING_FOR_IDLE
2755-
# We use 'elif' here because if there are tasks in
2756-
# wait_all_tasks_blocked, then those tasks will wake up without
2757-
# jumping the clock, so we don't need to autojump.
2758-
elif runner.clock.autojump_threshold < timeout:
2759-
timeout = runner.clock.autojump_threshold
2760-
idle_primed = IdlePrimedTypes.AUTOJUMP_CLOCK
2756+
2757+
timeout = min(max(0, timeout), _MAX_TIMEOUT)
2758+
real_timeout, virtual_timeout = runner.clock.deadline_to_sleep_time(timeout)
27612759

27622760
if "before_io_wait" in runner.instruments:
2763-
runner.instruments.call("before_io_wait", timeout)
2761+
runner.instruments.call("before_io_wait", real_timeout)
27642762

27652763
# Driver will call io_manager.get_events(timeout) and pass it back
27662764
# in through the yield
2767-
events = yield timeout
2765+
events = yield real_timeout
2766+
2767+
new_now = runner.clock.current_time()
2768+
runner.clock.propagate(new_now - now, virtual_timeout)
2769+
27682770
runner.io_manager.process_events(events)
27692771

27702772
if "after_io_wait" in runner.instruments:
2771-
runner.instruments.call("after_io_wait", timeout)
2773+
runner.instruments.call("after_io_wait", real_timeout)
27722774

27732775
# Process cancellations due to deadline expiry
27742776
now = runner.clock.current_time()
@@ -2805,9 +2807,6 @@ def unrolled_run(
28052807
runner.reschedule(task)
28062808
else:
28072809
break
2808-
else:
2809-
assert idle_primed is IdlePrimedTypes.AUTOJUMP_CLOCK
2810-
runner.clock.autojump()
28112810

28122811
# Process all runnable tasks, but only the ones that are already
28132812
# runnable now. Anything that becomes runnable during this cycle

src/trio/_core/_tests/test_guest_mode.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,6 @@ def current_time(self) -> float:
187187
def deadline_to_sleep_time(self, deadline: float) -> float:
188188
raise NotImplementedError()
189189

190-
@property
191-
def autojump_threshold(self) -> float:
192-
raise NotImplementedError()
193-
194-
def autojump(self) -> None:
195-
raise NotImplementedError()
196-
197190
def after_start_never_runs() -> None: # pragma: no cover
198191
pytest.fail("shouldn't get here")
199192

src/trio/_core/_tests/test_mock_clock.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ def test_mock_clock() -> None:
2424
with pytest.raises(ValueError, match=r"^time can't go backwards$"):
2525
c.jump(-1)
2626
assert c.current_time() == 1.2
27-
assert c.deadline_to_sleep_time(1.1) == 0
28-
assert c.deadline_to_sleep_time(1.2) == 0
29-
assert c.deadline_to_sleep_time(1.3) > 999999
27+
assert c.deadline_to_sleep_time(-0.1) == (0, 0)
28+
assert c.deadline_to_sleep_time(0.0) == (0, 0)
29+
assert c.deadline_to_sleep_time(0.1) ==(999999999, 0.1)
3030

3131
with pytest.raises(ValueError, match=r"^rate must be >= 0$"):
3232
c.rate = -1
@@ -36,15 +36,15 @@ def test_mock_clock() -> None:
3636
assert c.current_time() == 1.2
3737
REAL_NOW += 1
3838
assert c.current_time() == 3.2
39-
assert c.deadline_to_sleep_time(3.1) == 0
40-
assert c.deadline_to_sleep_time(3.2) == 0
41-
assert c.deadline_to_sleep_time(4.2) == 0.5
39+
assert c.deadline_to_sleep_time(-0.1) == (0, 0)
40+
assert c.deadline_to_sleep_time(0.0) == (0, 0)
41+
assert c.deadline_to_sleep_time(1.0) == (0.5, 1.0)
4242

4343
c.rate = 0.5
4444
assert c.current_time() == 3.2
45-
assert c.deadline_to_sleep_time(3.1) == 0
46-
assert c.deadline_to_sleep_time(3.2) == 0
47-
assert c.deadline_to_sleep_time(4.2) == 2.0
45+
assert c.deadline_to_sleep_time(-0.1) == (0, 0)
46+
assert c.deadline_to_sleep_time(0.0) == (0, 0)
47+
assert c.deadline_to_sleep_time(1.0) == (2.0, 1.0)
4848

4949
c.jump(0.8)
5050
assert c.current_time() == 4.0
@@ -80,11 +80,12 @@ async def test_mock_clock_autojump(mock_clock: MockClock) -> None:
8080
t = _core.current_time()
8181
# this should wake up before the autojump threshold triggers, so time
8282
# shouldn't change
83+
# TODO: technically, this waits for an infinite virtual time - how should `current_time()` change?
8384
await wait_all_tasks_blocked()
8485
assert t == _core.current_time()
8586
# this should too
8687
await wait_all_tasks_blocked(0.01)
87-
assert t == _core.current_time()
88+
assert t + 0.01 == _core.current_time()
8889

8990
# set up a situation where the autojump task is blocked for a long long
9091
# time, to make sure that cancel-and-adjust-threshold logic is working
@@ -179,15 +180,16 @@ async def waiter() -> None:
179180

180181

181182
async def test_initialization_doesnt_mutate_runner() -> None:
183+
# TODO: is this test even necessary now?
182184
before = (
183185
GLOBAL_RUN_CONTEXT.runner.clock,
184-
GLOBAL_RUN_CONTEXT.runner.clock.autojump_threshold,
186+
#GLOBAL_RUN_CONTEXT.runner.clock.autojump_threshold,
185187
)
186188

187189
MockClock(autojump_threshold=2, rate=3)
188190

189191
after = (
190192
GLOBAL_RUN_CONTEXT.runner.clock,
191-
GLOBAL_RUN_CONTEXT.runner.clock.autojump_threshold,
193+
#GLOBAL_RUN_CONTEXT.runner.clock.autojump_threshold,
192194
)
193195
assert before == after

0 commit comments

Comments
 (0)