Skip to content

Commit 4c63ecf

Browse files
pfalcondpgeorge
authored andcommitted
uasyncio.core: Switch to separate run and wait queues.
Instead of using single priority queue for all tasks, split into using "run queue", which represents tasks not waiting until specific time, which should be run on every (well, next) loop iteration, and wait queue, still a priority queue. Run queue is a simple FIFO, implemented by ucollections.deque, recently introduced in pfalcon/micropython. Thus, there's minimal storage overhead and intrinsic scheduling fairness. Generally, run queue should hold both a callback/coro and its arguments, but as we don't feed any send args into coros still, it's optimized to hold just 1 items for coros, while 2 for callbacks. Introducing run queue will also allow to get rid of tie-breaking counter in utimeq implementation, which was introduced to enforce fair scheduling. It's no longer needed, as all tasks which should be run at given time are batch-removed from wait queue and batch-inserted into run queue. So, they may be executed not in the order scheduled (due to non-stable order of heap), but the whole batch will be executed "atomically", and any new schedulings from will be processed no earlier than next loop iteration.
1 parent ab3198e commit 4c63ecf

File tree

1 file changed

+62
-39
lines changed

1 file changed

+62
-39
lines changed

uasyncio.core/uasyncio/core.py

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import utime as time
22
import utimeq
3+
import ucollections
34

45

56
type_gen = type((lambda: (yield))())
@@ -25,8 +26,9 @@ class TimeoutError(CancelledError):
2526

2627
class EventLoop:
2728

28-
def __init__(self, len=42):
29-
self.q = utimeq.utimeq(len)
29+
def __init__(self, runq_len=16, waitq_len=16):
30+
self.runq = ucollections.deque((), runq_len, True)
31+
self.waitq = utimeq.utimeq(waitq_len)
3032
# Current task being run. Task is a top-level coroutine scheduled
3133
# in the event loop (sub-coroutines executed transparently by
3234
# yield from/await, event loop "doesn't see" them).
@@ -41,18 +43,24 @@ def create_task(self, coro):
4143
# CPython asyncio incompatibility: we don't return Task object
4244

4345
def call_soon(self, callback, *args):
44-
self.call_at_(self.time(), callback, args)
46+
if __debug__ and DEBUG:
47+
log.debug("Scheduling in runq: %s", (callback, args))
48+
self.runq.append(callback)
49+
if not isinstance(callback, type_gen):
50+
self.runq.append(args)
4551

4652
def call_later(self, delay, callback, *args):
4753
self.call_at_(time.ticks_add(self.time(), int(delay * 1000)), callback, args)
4854

4955
def call_later_ms(self, delay, callback, *args):
56+
if not delay:
57+
return self.call_soon(callback, *args)
5058
self.call_at_(time.ticks_add(self.time(), delay), callback, args)
5159

5260
def call_at_(self, time, callback, args=()):
5361
if __debug__ and DEBUG:
54-
log.debug("Scheduling %s", (time, callback, args))
55-
self.q.push(time, callback, args)
62+
log.debug("Scheduling in waitq: %s", (time, callback, args))
63+
self.waitq.push(time, callback, args)
5664

5765
def wait(self, delay):
5866
# Default wait implementation, to be overriden in subclasses
@@ -64,45 +72,45 @@ def wait(self, delay):
6472
def run_forever(self):
6573
cur_task = [0, 0, 0]
6674
while True:
67-
if self.q:
68-
# wait() may finish prematurely due to I/O completion,
69-
# and schedule new, earlier than before tasks to run.
70-
while 1:
71-
t = self.q.peektime()
72-
tnow = self.time()
73-
delay = time.ticks_diff(t, tnow)
74-
if delay < 0:
75-
delay = 0
76-
# Always call wait(), to give a chance to I/O scheduling
77-
self.wait(delay)
78-
if delay == 0:
79-
break
80-
81-
self.q.pop(cur_task)
82-
t = cur_task[0]
83-
cb = cur_task[1]
84-
args = cur_task[2]
75+
# Expire entries in waitq and move them to runq
76+
tnow = self.time()
77+
while self.waitq:
78+
t = self.waitq.peektime()
79+
delay = time.ticks_diff(t, tnow)
80+
if delay > 0:
81+
break
82+
self.waitq.pop(cur_task)
83+
if __debug__ and DEBUG:
84+
log.debug("Moving from waitq to runq: %s", cur_task[1])
85+
self.call_soon(cur_task[1], *cur_task[2])
86+
87+
# Process runq
88+
l = len(self.runq)
89+
if __debug__ and DEBUG:
90+
log.debug("Entries in runq: %d", l)
91+
while l:
92+
cb = self.runq.popleft()
93+
l -= 1
94+
args = ()
95+
if not isinstance(cb, type_gen):
96+
args = self.runq.popleft()
97+
l -= 1
98+
if __debug__ and DEBUG:
99+
log.info("Next callback to run: %s", (cb, args))
100+
cb(*args)
101+
continue
102+
85103
if __debug__ and DEBUG:
86-
log.debug("Next coroutine to run: %s", (t, cb, args))
104+
log.info("Next coroutine to run: %s", (cb, args))
87105
self.cur_task = cb
88-
# __main__.mem_info()
89-
else:
90-
self.wait(-1)
91-
# Assuming IO completion scheduled some tasks
92-
continue
93-
if callable(cb):
94-
cb(*args)
95-
else:
96106
delay = 0
97107
try:
98-
if __debug__ and DEBUG:
99-
log.debug("Coroutine %s send args: %s", cb, args)
100-
if args == ():
108+
if args is ():
101109
ret = next(cb)
102110
else:
103111
ret = cb.send(*args)
104112
if __debug__ and DEBUG:
105-
log.debug("Coroutine %s yield result: %s", cb, ret)
113+
log.info("Coroutine %s yield result: %s", cb, ret)
106114
if isinstance(ret, SysCall1):
107115
arg = ret.arg
108116
if isinstance(ret, SleepMs):
@@ -147,7 +155,22 @@ def run_forever(self):
147155
# Currently all syscalls don't return anything, so we don't
148156
# need to feed anything to the next invocation of coroutine.
149157
# If that changes, need to pass that value below.
150-
self.call_later_ms(delay, cb)
158+
if delay:
159+
self.call_later_ms(delay, cb)
160+
else:
161+
self.call_soon(cb)
162+
163+
# Wait until next waitq task or I/O availability
164+
delay = 0
165+
if not self.runq:
166+
delay = -1
167+
if self.waitq:
168+
tnow = self.time()
169+
t = self.waitq.peektime()
170+
delay = time.ticks_diff(t, tnow)
171+
if delay < 0:
172+
delay = 0
173+
self.wait(delay)
151174

152175
def run_until_complete(self, coro):
153176
def _run_and_stop():
@@ -195,10 +218,10 @@ class IOWriteDone(SysCall1):
195218

196219
_event_loop = None
197220
_event_loop_class = EventLoop
198-
def get_event_loop(len=42):
221+
def get_event_loop(runq_len=16, waitq_len=16):
199222
global _event_loop
200223
if _event_loop is None:
201-
_event_loop = _event_loop_class(len)
224+
_event_loop = _event_loop_class(runq_len, waitq_len)
202225
return _event_loop
203226

204227
def sleep(secs):

0 commit comments

Comments
 (0)