RAM Mem Alloc fail even with half space free

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
Post Reply
thalesmaoa
Posts: 35
Joined: Thu Feb 13, 2020 10:10 pm

RAM Mem Alloc fail even with half space free

Post by thalesmaoa » Mon Jul 05, 2021 9:13 pm

Hi there, please, be sure that I've did everything that I could to debug it. I also know this is too much subjective, but I really need some help here.

Code: Select all

RAM free 38256 alloc 72912
Traceback (most recent call last):
  File "main.py", line 260, in <module>
  File "uasyncio/core.py", line 1, in run
  File "uasyncio/core.py", line 1, in run_until_complete
  File "uasyncio/core.py", line 1, in run_until_complete
  File "main.py", line 21, in main
MemoryError: memory allocation failed, allocating 4096 bytes
I'm using mqtt_as.py, ssd1306.py and ulab-micropython compilation. I'm running on a ESP32 TTGO.

Code: Select all

Chip is ESP32D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 26MHz
MAC: 24:6f:28:97:7a:f4
Uploading stub...
Running stub...
Stub running...
Manufacturer: ef
Device: 4018
Detected flash size: 16MB


Basically it gives me memory allocation error with a lot of free space.
I first considered that there are too much fragmentation inside my heap. I do manual gc all over, but it doesn't fix. I also don't have dynamic alloc (at least is what I think, because I'm not sure what happens inside mqtt_as, ssd1306 and ulab.

I have no problem to share the entire code. Not so sure where is the best spot.
I have no idea at all of what else I can do.

Code: Select all

async def main(client):
    gc.collect()
    global Va_np, Vb_np, Vc_np
    Va_rms, Vb_rms, Vc_rms = None, None, None
    Va_THD, Vb_THD, Vc_THD = None, None, None
    await client.connect()
    while True:
        #sleep_ms(2000)
        read_ok = await read_serial_values(uart_esp32)
        gc.collect()
        if read_ok:
            # print('1 RAM free {} alloc {}'.format(gc.mem_free(), gc.mem_alloc()))
            Va_np = calibra(Va, offset_A, max_A)
            Vb_np = calibra(Vb, offset_B, max_B)
            Vc_np = calibra(Vc, offset_C, max_C)
            Va_rms = rms(Va_np)
            Vb_rms = rms(Vb_np)
            Vc_rms = rms(Vc_np)
            Va_THD = THD(Va_np)
            Vb_THD = THD(Vb_np) 
            Vc_THD = THD(Vc_np)
            # print('2 RAM free {} alloc {}'.format(gc.mem_free(), gc.mem_alloc()))
            print(Va_rms, Vb_rms, Vc_rms)
            drawScreen(oled, Va_rms, Vb_rms, Vc_rms, Va_THD, Vb_THD, Vc_THD)
            await publishRMS(client, [Va_rms, Vb_rms, Vc_rms])
            await publishTHD(client, [Va_THD, Vb_THD, Vc_THD])
            drawStatus(oled)
 
def calibra(array, offset, max):
    x = 179.61 / max
    a_np = ( np.array( array ) - offset ) * x
    # print('Calibra RAM free {} alloc {}'.format(gc.mem_free(), gc.mem_alloc()))
    return a_np
    
def THD(array):
    """Calculate the THD"""
    Y = spy.signal.spectrogram(array)
    Vfun = 2*Y[20]/sr
    h_max = 25
    sum_abs = 0  
    # # Sum all the amplitudes 
    h_max = 25 # Max harmonic order
    for i_h in np.arange(40,20*(h_max+1),20):
        sum_abs += (Y[i_h]/sr)*2
    del Y
    gc.collect()
    return 100 * np.sqrt( sum_abs ) / Vfun

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: RAM Mem Alloc fail even with half space free

Post by jimmo » Tue Jul 06, 2021 4:05 am

thalesmaoa wrote:
Mon Jul 05, 2021 9:13 pm
I have no problem to share the entire code. Not so sure where is the best spot.
I have no idea at all of what else I can do.
This does look like fragmentation.

From a quick look at your code, one of the areas that might be worth investigating is that if you do an allocation and re-assign an existing variable, the old variable's memory can't be freed until afterwards.

i.e.

Code: Select all

a = something()
a = something_else()
then you can't free whatever was returned from "something" until after something_else completes. (This seems like it should be easy to optimise, but you don't know whether something_else will raise an exception).

The classic way this happens is:

Code: Select all

def foo():
  while True:
    a = get_something()
    ... do more work ...
So the solution is:

a) (Preferable) avoid re-assigning variables, instead put things into short functions and let them go out of scope.

So:

Code: Select all

def bar():
  a = get_something()
  ... do more work
  
def foo():
  while True:
    bar()
b) (Messy) Explictly "del" the variable before re-assigning.

You might find this talk helpful: https://www.youtube.com/watch?v=H_xq8IYjh2w

The other problem I see is that your code involves a lot of floating point, which unfortunately involves a heap allocation for each new float value calculated.

thalesmaoa
Posts: 35
Joined: Thu Feb 13, 2020 10:10 pm

Re: RAM Mem Alloc fail even with half space free

Post by thalesmaoa » Tue Jul 06, 2021 3:38 pm

Thanks. I saw the video last weekend. It gave me some ideas. I do believe floating point is the problem as well. Still, I'm confused about something.

1. If I declare a global variable, that space is reserved, right? What if I reassign it over and over?

Code: Select all

A = 0
def main():
	global A
	while True:
		A = get_something()
		. . .
I did that for my arrays, but I'm not sure if this is working as I think it should. Please, see Va, Vb, Vc, Va_np, Vb_np and Vc_np.
Va, Vb and Vc are int arrays
Va_np, Vb_np and Vc_np are np.arrays. This is the one that I keep re-assigning as you've mentioned.

Code: Select all

import gc
gc.enable()
gc.collect()

from micropython import const
from micropython import alloc_emergency_exception_buf
alloc_emergency_exception_buf(100)
gc.collect()

# In order to optimize heap allocation, declare variables
sr = const(1024) # Size of the ringbuffer = int(freq_sample/freq_grid*n_cycles)

# oLed screen size
oled_width = const(128)
oled_height = const(64)

# Offset and calibration values for ADC read - Values received
offset_A = const(2057)
offset_B = const(2057)
offset_C = const(2057)
max_A = const(876)
max_B = const(900)
max_C = const(900)

px_s = 0 # Aux variable that shows that the program is running

# MQTT variables
SERVER = b'192.168.200.254'
client_id = b'saguard-01'
topic_sub = b'saguard/+'
gc.collect()

# Create ringbuffer to store voltages
Va = [0]*sr
Vb = [0]*sr
Vc = [0]*sr
Vsag = [0]*sr
gc.collect()

from ulab import numpy as np
gc.collect()

from ulab import scipy as spy
gc.collect()

Va_np = np.zeros(sr)
Vb_np = np.zeros(sr)
Vc_np = np.zeros(sr)
gc.collect()
This is inside boot.py. I first declare it to freeze the RAM (at least is what I think it is doing).
After that, I just reassign inside my main function:

Code: Select all

async def main(client):
    gc.collect()
    global Va_np, Vb_np, Vc_np
    Va_rms, Vb_rms, Vc_rms = None, None, None
    Va_THD, Vb_THD, Vc_THD = None, None, None
    await client.connect()
    while True:
        #sleep_ms(2000)
        read_ok = await read_serial_values(uart_esp32)
        gc.collect()
        if read_ok:
            Va_np = calibra(Va, offset_A, max_A)
            Vb_np = calibra(Vb, offset_B, max_B)
            Vc_np = calibra(Vc, offset_C, max_C)
            Va_rms = rms(Va_np)
            Vb_rms = rms(Vb_np)
            Vc_rms = rms(Vc_np)
            Va_THD = THD(Va_np)
            Vb_THD = THD(Vb_np) 
            Vc_THD = THD(Vc_np)
            print(Va_rms, Vb_rms, Vc_rms)
            drawScreen(oled, Va_rms, Vb_rms, Vc_rms, Va_THD, Vb_THD, Vc_THD)
            await publishRMS(client, [Va_rms, Vb_rms, Vc_rms])
            await publishTHD(client, [Va_THD, Vb_THD, Vc_THD])
            drawStatus(oled)
You are saying that each time is changing the pointer?
I will try to check that.

EDIT:

Yes. Pointer is changing.

Code: Select all

0x3ffeea20 0x3ffeed10 0x3ffee930
0x3ffead50 0x3ffeaf40 0x3ffead20
0x3ffeade0 0x3ffe6c40 0x3ffeae10
0x3ffeadb0 0x3ffe6c70 0x3ffeade0
I start to see things a bit more clear now. It does not copy my result to the first pointer address. It just change the pointed over and over. Am I write?

If so, I may have two options:
1) Try to freeing memory as you mentioned.
2) Copy the result for the first pointer location in order to keep heap in order and avoid frag. The question is: is this a good approach and worth doing so? Besides, how to perform that? Maybe a for loop...

thalesmaoa
Posts: 35
Joined: Thu Feb 13, 2020 10:10 pm

Re: RAM Mem Alloc fail even with half space free

Post by thalesmaoa » Tue Jul 06, 2021 7:38 pm

I've followed your suggested approach.

Code: Select all

async def main(client):
    gc.collect()
    # global Va_np, Vb_np, Vc_np
    Va_rms, Vb_rms, Vc_rms = None, None, None
    Va_THD, Vb_THD, Vc_THD = None, None, None
    await client.connect()
    while True:
        #sleep_ms(2000)
        read_ok = await read_serial_values(uart_esp32)
        gc.collect()
        if read_ok:
            Va_rms, Vb_rms, Vc_rms, Va_THD, Vb_THD, Vc_THD = calcRMSTHD()
            print(Va_rms, Vb_rms, Vc_rms)
            drawScreen(oled, Va_rms, Vb_rms, Vc_rms, Va_THD, Vb_THD, Vc_THD)
            await publishRMS(client, [Va_rms, Vb_rms, Vc_rms])
            await publishTHD(client, [Va_THD, Vb_THD, Vc_THD])
            drawStatus(oled)

@micropython.native
def calcRMSTHD():
    Va_np = calibra(Va, offset_A, max_A)
    Vb_np = calibra(Vb, offset_B, max_B)
    Vc_np = calibra(Vc, offset_C, max_C)
    Va_rms = rms(Va_np)
    Vb_rms = rms(Vb_np)
    Vc_rms = rms(Vc_np)
    Va_THD = THD(Va_np)
    Vb_THD = THD(Vb_np) 
    Vc_THD = THD(Vc_np)
    return Va_rms, Vb_rms, Vc_rms, Va_THD, Vb_THD, Vc_THD
Indeed, it took now almost 6 hours to throw alloc error, but it is still there.

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: RAM Mem Alloc fail even with half space free

Post by jimmo » Thu Jul 08, 2021 1:07 am

thalesmaoa wrote:
Tue Jul 06, 2021 7:38 pm
I've followed your suggested approach.
I'm not quite sure this does what I was suggesting. Your code still has the pattern of

Code: Select all

def main():
  while True:
    x = something()
thalesmaoa wrote:
Tue Jul 06, 2021 3:38 pm
1. If I declare a global variable, that space is reserved, right? What if I reassign it over and over?
Global doesn't change anything here. It just affects how the variable name is looked up. Just think of the variable as a reference to some allocated memory, and as long as that reference still exists, the memory cannot be freed.

thalesmaoa
Posts: 35
Joined: Thu Feb 13, 2020 10:10 pm

Re: RAM Mem Alloc fail even with half space free

Post by thalesmaoa » Fri Jul 09, 2021 12:49 pm

Thanks. I am sorry to insist. It really changes the way to think about microcontrollers. Let me put it visual:

Image

I was expecting that manual copying the result, not only the pointed, would fix the fragmentation problem. What I'm not seeing?

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: RAM Mem Alloc fail even with half space free

Post by jimmo » Sat Jul 10, 2021 12:22 pm

thalesmaoa wrote:
Fri Jul 09, 2021 12:49 pm
I was expecting that manual copying the result, not only the pointed, would fix the fragmentation problem. What I'm not seeing?
I'm just not quite following what you mean by "manual copying the result". MicroPython doesn't give you any way to "move" data around in RAM.

Your diagram looks correct though in terms of how the GC algorithm works in general and the sort of behavior that leads to fragmentation.

thalesmaoa
Posts: 35
Joined: Thu Feb 13, 2020 10:10 pm

Re: RAM Mem Alloc fail even with half space free

Post by thalesmaoa » Sat Jul 10, 2021 7:36 pm

I see. It is my misunderstanding.

My believe was that, having an global array I can latter assign its values and RAM would use prealloc space.

Code: Select all

A = [0]*10

def main:
	for i in range( len(A) ):
		A[i] = 2*2;
But I get it now. Thank you.

Besides, I really can't get it to stop with Mem alloc error.
I did another mod:

Code: Select all

async def main(client):
    gc.collect()
    await client.connect()
    while True:
        await runTasks(oled)

async def runTasks(led):
    read_ok = await read_serial_values(uart_esp32)
    if read_ok:
        Va_rms, Vb_rms, Vc_rms, Va_THD, Vb_THD, Vc_THD = calcRMSTHD()
        print(Va_rms, Vb_rms, Vc_rms)
        drawScreen(led, Va_rms, Vb_rms, Vc_rms, Va_THD, Vb_THD, Vc_THD)
        await publishRMS(client, [Va_rms, Vb_rms, Vc_rms])
        await publishTHD(client, [Va_THD, Vb_THD, Vc_THD])
        drawStatus(led)
It stop when I convert a normal array to np.array.

Full code is here:
https://github.com/thalesmaoa/esp32-oled-RMSTHD

If you can, please, take a look. I really appreciate.

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: RAM Mem Alloc fail even with half space free

Post by jimmo » Sun Jul 11, 2021 2:07 pm

thalesmaoa wrote:
Sat Jul 10, 2021 7:36 pm
My believe was that, having an global array I can latter assign its values and RAM would use prealloc space.
That is true. (And not just for a global array, for any array).

But I didn't see any arrays in your code.

Also note that the array slots are re-used, but if you're storing objects (not integers) into the array then they do not get moved.
thalesmaoa wrote:
Sat Jul 10, 2021 7:36 pm
Besides, I really can't get it to stop with Mem alloc error.
That's good, right? You can't make your program stop any more?
thalesmaoa wrote:
Sat Jul 10, 2021 7:36 pm
It stop when I convert a normal array to np.array.
What do you mean by stop?

thalesmaoa
Posts: 35
Joined: Thu Feb 13, 2020 10:10 pm

Re: RAM Mem Alloc fail even with half space free

Post by thalesmaoa » Mon Jul 12, 2021 12:25 pm

I'm sorry. I choose the wrong words. English is not my mother language.
When I use "stop" means "quit".

I have three global arrays and I still getting an error.

My code is:

1) I receive an array over UART.
2) Convert the array to numpy.array (sometime a get error with memory allocation).
3) Calc float values from array
4) Free RAM

Post Reply