Delay_ms leak
See original GitHub issueHello Peter, first, thank you very much for this incredible library. Asyncio on micropython has been game changer for embedded applications.
Our application is fairly complex, we use context managers to build up and tear down the application in a controlled way. We are noticing we have a memory leak, and I believe I’ve narrowed it down to Delay_ms.
Give it a try:
import sys
import machine
import gc
import uasyncio as asyncio
from primitives.delay_ms import Delay_ms
async def main():
    async def scoped():
        nop = Delay_ms()
        await asyncio.sleep(.25)
    while True:
        await scoped()
        gc.collect()
        print(gc.mem_free())
try:
    asyncio.run(main())
except KeyboardInterrupt:
    pass
except Exception as err:
    sys.print_exception(err)
finally:
    asyncio.new_event_loop()
    gc.collect()
In primitives/Delay_ms, last line of init, the task is created but never canceled. If you simply comment out that line, and re-run the test above, you’ll see there’s no loss.
def __init__(self, func=None, args=(), duration=1000):
        self._func = func
        self._args = args
        self._durn = duration  # Default duration
        self._retn = None  # Return value of launched callable
        self._tend = None  # Stop time (absolute ms).
        self._busy = False
        self._trig = asyncio.ThreadSafeFlag()
        self._tout = asyncio.Event()  # Timeout event
        self.wait = self._tout.wait  # Allow: await wait_ms.wait()
        self._ttask = self._fake  # Timer task
        asyncio.create_task(self._run())    ############### TASK CREATED BUT NEVER KILLED
The fix for me…
In my code, I’ve done the following, in init
self._mtask = asyncio.create_task(self._run()) #Main task
and created a new function:
def deinit(self):
        self._mtask.cancel()
New Test case:
import sys
import machine
import gc
import uasyncio as asyncio
from primitives.delay_ms import Delay_ms
async def main():
    async def scoped():
        try:
            nop = Delay_ms()
        finally:
            nop.deinit()
        await asyncio.sleep(.25)
    while True:
        await scoped()
        gc.collect()
        print(gc.mem_free())
try:
    asyncio.run(main())
except KeyboardInterrupt:
    pass
except Exception as err:
try:
    asyncio.run(main())
except KeyboardInterrupt:
    pass
except Exception as err:
    sys.print_exception(err)
finally:
    asyncio.new_event_loop()
    gc.collect()
I’m not 100% clear on del operation in micropython, if it’s guaranteed, but I think being explicit with a deinit function seems to make sense as other micropython objects also deinit.
Issue Analytics
- State:
- Created 2 years ago
- Comments:7 (4 by maintainers)

 Top Related Medium Post
Top Related Medium Post Top Related StackOverflow Question
Top Related StackOverflow Question
Documentation update is great, I would have made sense of this. Thank you again for your attention to details and taking up this improvement.
I have made a minor change. I was concerned that, after issuing
.deinit, the timer was in a non-working state. Attempts to retrigger it would fail silently. Now, in this event, aRuntimeErroris raised. This is tested intests/delay_test.py. The tutorial now has a brief explanation of the issue.The following note is so that I have a record of the design criteria.
Design
I investigated a design which avoided a
._run()task, starting and cancelling a timer task as required. This would have had the advantage that an inactive instance which went out of scope would leave no running task. However maintaining the ability to trigger in a hard ISR proved problematic. Doubtless the issues withmicropython.schedulecould be fixed, but the code was starting to look rather elaborate. Further, it didn’t fix the problem in the general case, as a running delay which went our of scope would still leave a running task. It would still be necessary to issue.stopto ensure clean termination.I have therefore retained the existing design.