New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC asyncio: efficiently polling devices #2664
Comments
This is indeed how the EpollEventLoop works: in the case where there is no work to do (no coros are active) this scheduler will call To apply this to other hardware devices those devices would need to be pollable by the |
That would be a great enhancement. Is it a major task? |
No, it's not too difficult. If your object provides an |
[edited] Looking at the code it seems to me that Or is there another established approach? Does the |
It works differently to this. The main event loop which schedules the coros is the only part of the code that calls select, and hence the only part that blocks. The coros that want to do IO should never block, they should only do non-blocking IO and should not call select (except with a zero timeout, ie a simple poll). If they need to wait, the coros should yield a special IO object (eg IORead) and then the event loop collects all the IO operations that are waiting, configures select/poll to listen for all these IO ops, and then blocks until one or more of them are ready. |
OK, I've found the code. A clever solution! I'll experiment. |
I'm experiencing a couple of problems. I've written a dummy Device class using IORead whose ioctl method will currently always return 0. The class has a fileno() method, which I gather needs to return the address of the device as an integer. Firstly, I don't know a way to do this in Python (uctypes.addressof() works only for buffers). I used assembler but a Python solution would be good. Secondly, on the Pyboard an OSError is thrown. On Unix I tried returning an arbitrary integer from fileno(). No error was thrown, the readloop running continuously (unsurprisingly ioctl was never called). On the Pyboard the same code, returning the same integer, threw the OSError. Here is the code and the outcome on the Pyboard. try:
import uasyncio as asyncio
except ImportError:
import asyncio
MP_STREAM_POLL_RD = 1
MP_STREAM_POLL = 3
# Get address of object not supporting the buffer protocol
@micropython.asm_thumb
def addressof(r0):
nop()
class Device(object):
def __init__(self):
self.ready = False
def fileno(self):
print(hex(addressof(self)))
return addressof(self)
def ioctl(self, cmd, flags):
res = 0
print('Got here')
if cmd == MP_STREAM_POLL and (flags & MP_STREAM_POLL_RD):
if self.ready:
res = MP_STREAM_POLL_RD
return res
async def readloop(self):
while True:
print('About to yield')
yield asyncio.IORead(self)
print('Should never happen')
loop = asyncio.get_event_loop()
device = Device()
loop.call_soon(device.readloop())
loop.run_forever() Outcome:
Doubtless I'm missing something, but a pointer would be great :) |
With respect to your uasync examples here, you could also use the The content of the queue could contain the state change of a switch ( The PushButton class could have the attributes Any idea if there is any performance difference in using a queue between existing coros, or a coro creating other coros on the fly ? |
Those notes are a work in progress. I'd regard the Switch, Pushbutton and Delay_ms classes as reasonably developed as they were ported from another scheduler but I'll bear in mind your naming suggestion. The notes - which I've now split out into a separate document - are probably best ignored at this stage. I have some code samples using I'd very much like to know how to fix this |
I found the following in Do you need I'm not sure how a protocol is set, or how the flags fields get set in python objects. Inherited from a base class perhaps? There must be examples of other stream objects that would give some insight.
|
In essence my problem is this. I'm emulating a device driver for a read-only device. For efficiency I want to delegate polling the device to the scheduler. @dpgeorge advised that the way to do this is to have an In order for this mechanism to work, I need a
Doubtless I'm missing something simple here, but a pointer would be much appreciated :) |
@BrendanSimon Following up your identification of the source of the error I modified mp_get_stream_raise() to produce debug info to determine the cause of the error. It's getting a null pointer. Line 21 of uasyncio's self.poller.register(fd, select.POLLIN) is getting fd == 0x2000a3b0 so the event loop's add_reader() method is retrieving the |
I'm also making no headway with using a UART which also has a fileno() issue. The following loopback test: import uasyncio as asyncio
from pyb import UART
uart = UART(1, 9600)
async def sender():
swriter = asyncio.StreamWriter(uart, {})
while True:
swriter.awrite('Hello uart\n')
await asyncio.sleep(2)
async def receiver():
sreader = asyncio.StreamReader(uart)
while True:
res = await sreader.readline()
print('Recieved', res)
loop = asyncio.get_event_loop()
loop.create_task(sender())
loop.create_task(receiver())
loop.run_forever() fails with
Similar instantiation of StreamReader and StreamWriter objects is found in open_connection() with a socket, so I'm puzzled why a UART is failing. |
It looks like fileno for a socket is only implemented in unix/file.c and unix/modsocket.c So right now (with no changes), it looks like only unix files and sockets will work with uasyncio |
Yes, but that's only the Python side of things... on the other side (in C) there needs to be a bit of glue code so that the Python ioctl method that the user defines can be accessed when a C method is expected. But aside from that, one of the main blockers for this issue is with fileno: on baremetal targets there is no such concept as file descriptor and so either uasyncio needs to be rewritten to support file "objects" rather than descriptors, or baremetal needs to support file descriptors. |
So I gather that on baremetal efficient polling and asynchronous UART I/O in Python are both dependent on firmware changes. Are these in the pipeline for implementation, or is it a longer term goal? Aside from the polling issue I'm deeply impressed by the new uasyncio which has done everything I've thrown at it :) |
As part of getting uasyncio working on esp8266 (and subsequently pyboard) there will need to be changes to make this stuff work (eg for esp8266 to do concurrent socket handling). |
With micropython/micropython-lib#164 the UART I/O is now working, but so far I've had no joy defining a Python class with an ioctl method.
Before spending time on this, has this been implemented yet? |
Nope, the idea of the streams is that they're efficient and thus should be implemented in C. It's a bit of oxymoron thus to want to implement them in Python, and unclear, if there's a real usecase except unit testing. @dpgeorge posted an example of different native subclassing technique(s) though, using streams as an illustration: #2824 |
Revisiting this after the welcome implementation of #3836. An application with multiple coroutines yielding on The use of this option brings two benefits:
There is a drawback in that raw scheduling performance is reduced where I/O is not in use. In practice a user would only specify the option if fast I/O were to be used. I have benchmarked the proposed solution in the default situation where fast I/O is not selected and no I/O device is used: the reduction in raw scheduling performance is at most 3%. Current designWhen I/O is pending In Proposed solution
PerformanceA useful performance metric is the rate at which, in the absence of any I/O scheduling, Current code schedules up to 200 coros at a rate of ~6800Hz - in other words the minimum overhead is about 146μs. With As stated above, if The benefit is that if Potential issueThe current design's round-robin scheduling guarantees that, with properly written coroutines, an application can never block the scheduler. In the proposed solution this would remain the default. With
My view on these points is as follows:
ConclusionThis change would release the full potential of #3836 and it would go some way towards ameliorating UART buffering requirements as discussed in that PR (fixing the read/write bug won't, of course, eliminate the need for hardware buffering: this proposed "priority" patch reduces but does not eliminate scheduling latency). I am happy to attempt to implement this (preferably after dealing with the outstanding read/write issue) if the team agrees it's beneficial. Comments on the proposed solution are, of course, welcome. |
In the absence of any comments to this I have raised micropython-lib PR 287 implementing the above. |
Download links from S3; Do not upload release assets to GitHub
A common situation when interfacing hardware is the case where a device which is normally not ready needs to be polled. One solution is along the lines of
This works but has a high worst case latency. If there are N coroutines each with a maximum latency of T between
yield
statements, an interval of up to NT will exist between calls to device.ready(). This can be reduced to T if there is a way to delegate the polling to the scheduler. In other words to provide a facility to schedule on an event (cf thePoller
class in my scheduler).Any comments on ways to achieve this with uasyncio? Perhaps there's a solution which I've missed.
To anyone interested I've ported my classes for debounced switches and pushbuttons to asyncio and plan to extend this to other drivers. Preliminary code and docs here.
The text was updated successfully, but these errors were encountered: