Skip to content

Make keypad select/poll'able, which leads to async goodness #6712

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

Merged
merged 1 commit into from
Aug 10, 2022

Conversation

jepler
Copy link

@jepler jepler commented Aug 8, 2022

This allows a small wrapper class to be written

class AsyncEventQueue:
    def __init__(self, events):
        self._events = events

    async def __await__(self):
        await asyncio.core._io_queue.queue_read(self._events)
        return self._events.get()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

and used to just "await" the next event:

async def key_task():
    print("waiting for keypresses")
    with keypad.KeyMatrix([board.D4], [board.D5]) as keys, AsyncEventQueue(keys.events) as ev:
        while True:
            print(await ev)

@jepler
Copy link
Author

jepler commented Aug 9, 2022

While I haven't benchmarked this, I believe it's more efficient than an async def that would poll the EventQueue, like

async def aget(event_queue, interval):
    while True:
        if (event := event_queue.get()) is not None:
            return event
        await asyncio.sleep(interval)

because the checking for event availability happens without even entering Python bytecode, within select.poll. It's still polled internally, but just from efficient "C" code.

This allows a small wrapper class to be written
```py
class AsyncEventQueue:
    def __init__(self, events):
        self._events = events

    async def __await__(self):
        yield asyncio.core._io_queue.queue_read(self._events)
        return self._events.get()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

```
and used to just "await" the next event:
```py
async def key_task():
    print("waiting for keypresses")
    with keypad.KeyMatrix([board.D4], [board.D5]) as keys, AsyncEventQueue(keys.events) as ev:
        while True:
            print(await ev)
```

Because checking the empty status of the EventQueue does not enter
CircuitPython bytecode, it's assumed (but not measured) that this is
more efficient than an equivalent loop with an `await async.sleep(0)`
yield and introduces less latency than any non-zero sleep value.
@jepler jepler force-pushed the keyboard-keypad-ioctl branch from 7f71115 to 76f03a2 Compare August 10, 2022 02:50
@jepler
Copy link
Author

jepler commented Aug 10, 2022

Refined to use the existing common_hal_keypad_eventqueue_get_length in eventqueue_ioctl. The corresponding plain CircuitPython code would look like

async def aget(event_queue, interval):
    while not event_queue:
        await asyncio.sleep(interval)
    return event_queue.get()

@jepler
Copy link
Author

jepler commented Aug 10, 2022

I wrote a test program which runs on raspberry pi pico. It creates a separate Keys object and asyncio task for each GPIO, as well as a "logic task" which just counts how long it takes to await asyncio.sleep(0) a given number of times.

I tested with three ways of polling, and with 1 and 29 key tasks.

With 29 key tasks (all GPIOs used):

  • The new core polling code. This was best, at ~2000 logic loop iterations per second
  • The pure python polling code, sleep of 10ms between checks. ~146 logic loop iterations per second
  • The pure python polling code, sleep of 0 between checks. ~73 logic loop iterations per second

I also tested with just one Keys task:

  • the new core polling code: The best at ~2200 logic loop iterations per second
  • Pure python, 10ms: ~2100 logic loop iterations per second
  • Pure python, 0: ~1000 logic loop iterations per second

And with zero Keys tasks:

  • All variants gave ~2300s loops/second because the key polling code was not used

While this is a contrived example, it shows the large benefit to program responsiveness, especially as the number of tasks grows. Busy-polling 29 waiting tasks in a CircuitPython loop makes the compute task wait ~13.5ms each time it sleeps. Doing it with the core code is only ~0.5ms (and under 100 microseconds more than with 0 tasks to busy-poll). Even though it is still "just" a polling loop, it makes a huge difference that it can do a polling cycle without entering the Python VM.

Copy link
Collaborator

@dhalbert dhalbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this inspired change!

@dhalbert
Copy link
Collaborator

  • The pure python polling code, sleep of 10ms between checks. ~146 logic loop iterations per second
  • The pure python polling code, sleep of 0 between checks. ~73 logic loop iterations per second

Why do you think a 0 sleep is worse? That's confusing to me.

@dhalbert dhalbert merged commit ce2bd9b into adafruit:main Aug 10, 2022
@jepler
Copy link
Author

jepler commented Aug 10, 2022

Why do you think a 0 sleep is worse? That's confusing to me.

I think that when all the tasks are doing an asyncio.sleep(0) they all get polled every time the "logic task" sleeps. When the key-waiting tasks asyncio.sleep(.010) (10ms), then MOST of the time when the "logic task" does its asyncio.sleep(0) the key-waiting tasks don't execute at all.

@jepler
Copy link
Author

jepler commented Sep 21, 2023

I updated the initial comment code so that it works with current versions of asyncio. This code depends on internal details of the asyncio library and as such can be broken by internal changes to asyncio. (adafruit/Adafruit_CircuitPython_asyncio@fb068e2)

@jepler jepler deleted the keyboard-keypad-ioctl branch September 21, 2023 11:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants