CAN bus heartbeat receive using asyncio is slow

The official PYBD running MicroPython, and its accessories.
Target audience: Users with a PYBD
Post Reply
MicroRichard
Posts: 15
Joined: Tue Aug 09, 2022 11:32 am

CAN bus heartbeat receive using asyncio is slow

Post by MicroRichard » Wed Aug 10, 2022 2:31 pm

In my micropython project i have the following async continuous tasks:
led blink task => await asyncio.sleep(1)
battery task => await asyncio.sleep(2)
system task 1 => await asyncio.sleep(1)
system task 2 => await asyncio.sleep(1)
can bus task => can.any(), can.recv()
light sensor task => await asyncio.sleep(2)
webserver task => asyncio.start_server()
touch screen poll task => await asyncio.sleep(0.05)

These tasks are all in the form of:

Code: Select all

async def task(self):
   while(True)
      ...
      await asyncio.sleep(x secs)
(except for the can bus task which does not have the await asyncio.sleep())

and they are created using uasyncio.create_task(self.task()) in a function called start().
Further i have two CAN devices connected to the pyboard which each of them sends a heartbeat message every 200 msec. I have checked with a Kvaser that they send the heartbeat.
When only the can bus task is enabled, the received CAN heartbeats are around 200 msec but when i enable all tasks (especially the touch screen poll task) it seems micropython gets very busy and the time between CAN heartbeats go up to 400/500 and sometimes even 800 msecs.

How can i speed up tasks or even prioritise some tasks? Is it possible to have an insight in the performance of micropython as the async tasks are concerned? Or do i have to implement the CAN task in a different way?

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: CAN bus heartbeat receive using asyncio is slow

Post by pythoncoder » Thu Aug 11, 2022 10:32 am

It's hard to answer these kind of queries without a detailed trawl through the code, and even then it can be difficult. If you have access to a logic analyser you might want to consider micropython-monitpr which uses a Raspberry Pico to monitor asynchronous systems in real time. It is specifically designed to detect tasks that hog the CPU.
Peter Hinch
Index to my micropython libraries.

MicroRichard
Posts: 15
Joined: Tue Aug 09, 2022 11:32 am

Re: CAN bus heartbeat receive using asyncio is slow

Post by MicroRichard » Fri Aug 12, 2022 7:42 am

Thank you for reacting. For what i understand from the async library is that the tasks are scheduled round robin, so they are scheduled to run as soon as the running task yields (by calling async.sleep()). I am wondering if it is practically possible with micropython given the number of tasks and the heartbeats at 200 msec to service them in time. Suppose theoretically speaking 4 tasks take 50 msec each then the CAN receive task is already 200 msec behind.
Does it make sense to put timestamps at the start and end in the while loops of every task to see how long a task needs to execute?

Code: Select all

async def task(self):
   while(True)
      timestamp start
      ...
      timestamp end
      await asyncio.sleep(x secs)
What i did find already is that the touch screen polling task is a heavy burden. Perhaps use an ISR instead.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: CAN bus heartbeat receive using asyncio is slow

Post by pythoncoder » Fri Aug 12, 2022 8:26 am

Your comments on uasyncio scheduling are correct. It is a classic problem with asynchronous systems: figuring out where the CPU time is being used. You can use timestamps but you soon get bogged down in reams of data. You really can't beat a logic analyser for doing this kind of work. You can get very cheap Chinese Saleae clones, but they lack some of the functionality of the real thing.

A crucial task is to identify any code that blocks for significant periods. Polling an interface isn't necessarily a problem: I have systems that poll mechanical switches. The vast majority of the time a task reads a GPIO, detects nothing has changed, and goes to sleep for 50ms. uasyncio is sufficiently efficient that having several such tasks running uses minimal resources. However if your touch screen read blocks for tens of ms then you're in trouble, and using interrupts won't really help (it's naughty for ISR's to block for long).

Good luck. This stuff is hard - that's what makes it interesting ;)
Peter Hinch
Index to my micropython libraries.

MicroRichard
Posts: 15
Joined: Tue Aug 09, 2022 11:32 am

Re: CAN bus heartbeat receive using asyncio is slow

Post by MicroRichard » Fri Aug 12, 2022 1:01 pm

For every task that is running i am trying to improve speed by removing blocking calls etc. More than one task contains a callback however and i am not sure how to deal with those.
For example the LightSensorHandler below has a task that calls a callback function. The class Bootstrapper implements the callback and calls 2 other functions. Suppose these functions both take 100 msec, does this execution of the callback function also counts as the LightSensorHandler tasks duration? If so, is there a better way?

Code: Select all

class LightSensorHandler(object):
    def __init__(self, light_sensor_driver, event_cb):
        self._event_cb = event_cb
        self._driver = light_sensor_driver
        self._level = None
        
    async def task(self):
        while True: 
            level = self._driver.measure()
            if self._level != level:
                self._level = level
                if self._event_cb:
                    self._event_cb(LightSensorEvent.LIGHT_LEVEL_CHANGED, level)
                
            await asyncio.sleep(2.0)
    
class Bootstrapper:
    def __init__(self, backlight_handler, signal_led_handler):
        self.backlight_handler = backlight_handler
        self.signal_led_handler = signal_led_handler
        
    def light_level_changed_callback(self, event, level):
        if event == LightSensorEvent.LIGHT_LEVEL_CHANGED:
            self.backlight_handler.dim_light(level)
            self.signal_led_handler.dim_light(level)

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: CAN bus heartbeat receive using asyncio is slow

Post by pythoncoder » Mon Aug 15, 2022 7:36 am

To provide a general answer to this question, say you have an asynchronous function foo that calls a synchronous function bar. The function bar might call other synchronous functions. The scheduler will be blocked for the duration while bar runs. This can be measured as follows:

Code: Select all

from time import ticks_us, ticks_diff
async def foo():
    t = ticks_us()
    bar()  # Scheduler is blocked while this runs
    dt = ticks_diff(ticks_us(), t)
    print("bar duration", dt)
The above applies equally to methods or functions.

Periods in which the scheduler is blocked have an impact on all tasks so it is crucial to minimise blocking duration.
Peter Hinch
Index to my micropython libraries.

Post Reply