Skip to content

Memory leaking in the Delay_ms module #138

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

Closed
nesergen opened this issue Mar 29, 2025 · 3 comments
Closed

Memory leaking in the Delay_ms module #138

nesergen opened this issue Mar 29, 2025 · 3 comments

Comments

@nesergen
Copy link

nesergen commented Mar 29, 2025

Good day!
Board: stm32f411
OS: Micropython v1.23.0
I have found that when I use the Delay_ms module the board memory always became smaller and smaller even If I collect the garbage regularly. This code shows such behavior:

async def todo_3():
	print('todo_3' )

async def todo_2():
	print('start todo_2')
	d = Delay_ms(todo_3, duration=1000)
	d.trigger()
	print('end todo_2')

async def main():
	for _ in range(10):
		print("\nStart main")
		print("mem_free = ", gc.mem_free())

		await todo_2()

		await uasyncio.sleep_ms(2000)
		gc.collect()

		print("END main")


uasyncio.run(main())

In result memory starts leaking after three cycles :

Start main
00:06:31.108 -> mem_free =  72144
00:06:31.108 -> start todo_2
00:06:31.141 -> end todo_2
00:06:32.133 -> todo_3
00:06:33.125 -> collected_free =  72144
00:06:33.125 -> END main
00:06:33.125 -> 
00:06:33.125 -> Start main
00:06:33.125 -> mem_free =  73808
00:06:33.125 -> start todo_2
00:06:33.125 -> end todo_2
00:06:34.150 -> todo_3
00:06:35.143 -> collected_free =  73808
00:06:35.143 -> END main
00:06:35.143 -> 
00:06:35.143 -> Start main
00:06:35.143 -> mem_free =  73088
00:06:35.143 -> start todo_2
00:06:35.143 -> end todo_2
00:06:36.135 -> todo_3
00:06:37.127 -> collected_free =  73088
00:06:37.127 -> END main
00:06:37.127 -> 
00:06:37.127 -> Start main
00:06:37.160 -> mem_free =  72336
00:06:37.160 -> start todo_2
00:06:37.160 -> end todo_2
00:06:38.153 -> todo_3
00:06:39.145 -> collected_free =  72336
00:06:39.145 -> END main
00:06:39.145 -> 
00:06:39.145 -> Start main
00:06:39.145 -> mem_free =  71616
00:06:39.145 -> start todo_2
00:06:39.145 -> end todo_2
00:06:40.137 -> todo_3
00:06:41.162 -> collected_free =  71616
00:06:41.162 -> END main
00:06:41.162 -> 
00:06:41.162 -> Start main
00:06:41.162 -> mem_free =  70864
00:06:41.162 -> start todo_2
00:06:41.162 -> end todo_2
00:06:42.154 -> todo_3
00:06:43.147 -> collected_free =  70864
00:06:43.147 -> END main
00:06:43.147 -> 
00:06:43.147 -> Start main
00:06:43.147 -> mem_free =  70144
00:06:43.147 -> start todo_2
00:06:43.147 -> end todo_2
00:06:44.172 -> todo_3
00:06:45.164 -> collected_free =  70144
00:06:45.164 -> END main
00:06:45.164 -> 
00:06:45.164 -> Start main
00:06:45.164 -> mem_free =  69392
00:06:45.164 -> start todo_2
00:06:45.164 -> end todo_2
00:06:46.156 -> todo_3
00:06:47.181 -> collected_free =  69392
00:06:47.181 -> END main
00:06:47.181 -> 
00:06:47.181 -> Start main
00:06:47.181 -> mem_free =  68672
00:06:47.181 -> start todo_2
00:06:47.181 -> end todo_2
00:06:48.173 -> todo_3
00:06:49.166 -> collected_free =  68672
00:06:49.166 -> END main
00:06:49.166 -> 
00:06:49.166 -> Start main
00:06:49.166 -> mem_free =  67920
00:06:49.166 -> start todo_2
00:06:49.166 -> end todo_2
00:06:50.191 -> todo_3
00:06:51.183 -> collected_free =  67920
00:06:51.183 -> END main

In my real program I need to create an async task sometime that have to callback some function with time delay.

Then I tried the different approach and it works well:

def todo_2():
	print("todo_2 ")


async def schedule(callback, timer, *args, **kwargs):
	print("start schedul")
	await uasyncio.sleep(timer)
	callback(*args, **kwargs)


async def main():

	for _ in range(10):
		print("Start main")
		print("Free_mem = ",gc.mem_free())

		await uasyncio.create_task(schedule(todo_2, 1))
		await uasyncio.sleep(3)

		gc.collect()
		print("END main")

uasyncio.run(main())

Now I think, am I doing everything right with Delay_ms?

@peterhinch
Copy link
Owner

The problem arises because the code keeps creating new instances of Delay_ms.

I appreciate that MicroPython should detect that old instances are no longer referenced and GC them. I'm not sure why this isn't happening. However needlessly instantiating objects is poor practice. The following fixes this behaviour:

import asyncio
from primitives import Delay_ms
import gc

async def todo_3():
	print('todo_3' )

async def todo_2(d):
	print('start todo_2')
	d.trigger()
	print('end todo_2')

async def main():
	d = Delay_ms(todo_3, duration=1000)  # Instantiate once only
	for _ in range(10):
		print("\nStart main")
		print("mem_free = ", gc.mem_free())

		await todo_2(d)

		await asyncio.sleep_ms(2000)
		gc.collect()

		print("END main")

asyncio.run(main())

@peterhinch
Copy link
Owner

peterhinch commented Mar 30, 2025

On further thought, a second way to fix this is to call .deinit() before losing the Delay_ms reference. The reason for the memory leak is that each instance of Delay_ms creates a task which persists until .deinit() is called.

import asyncio
from primitives import Delay_ms
import gc

async def todo_3():
	print('todo_3' )

async def todo_2():
	d = Delay_ms(todo_3, duration=1000)
	print('start todo_2')
	d.trigger()
	print('end todo_2')
	d.deinit()

async def main():
	for _ in range(10):
		print("\nStart main")
		print("mem_free = ", gc.mem_free())

		await todo_2()

		await asyncio.sleep_ms(2000)
		gc.collect()

		print("END main")

asyncio.run(main())

While this works, the first suggestion is better for the reasons given.
[EDIT]
Added a note in the docs.

@nesergen
Copy link
Author

nesergen commented Apr 1, 2025

Thank you for the quick and complete answer.

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

No branches or pull requests

2 participants