Skip to content

Memory Leak After Disconnect (when acting as a BLE central) #146

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
300bps opened this issue Dec 9, 2021 · 2 comments · Fixed by #147
Closed

Memory Leak After Disconnect (when acting as a BLE central) #146

300bps opened this issue Dec 9, 2021 · 2 comments · Fixed by #147

Comments

@300bps
Copy link

300bps commented Dec 9, 2021

Issue: When acting as a ble central, there appears to be a persistent memory leak after disconnecting from a device.

Conditions: Circuitpython 7.0.0, mpy=517 library bundle: 7.x-mpy-20211125
Hardware: Feather nrf52840

Description: My application (acting as a central) connects to another adafruit board (Itsy nrf52840) acting as a peripheral, reads some data, and then disconnects. It repeats this process with one or more peripheral boards. I noticed that memory trends monotonically downward, even when explicitly requesting garbage collection, until the application crashes/freezes.

Duplication: I created the following code snippet that demonstrates the issue. Copy the code into code.py on a circuitpython system (see conditions I tested under at top). Replace the address in 'ble_target_address' with the address of a known, connectable device that you can use as the peripheral to connect to. Running the program will repeatedly connect, read services, and disconnect. Each time, it manually requests garbage collection and then prints the free memory. See a snippet of the output of a sample run below the code listing.

import gc, time

from adafruit_ble import BLERadio
from adafruit_ble.services.standard import GenericAccess
import _bleio

# BLE address (little-endian) of a known, connectable ble device
ble_target_address = _bleio.Address(bytes([0x16, 0x08, 0xb0, 0xf4, 0x69, 0xc3]), address_type=_bleio.Address.RANDOM_STATIC)
ble = BLERadio()

while True:
    try:
        conn = None
        try:
            conn = ble.connect(ble_target_address, timeout=2.0)            
            if conn:
                print("<Connect> ", end="")

                # Get services
                try:
                    genericaccess_service = conn[GenericAccess]
                except KeyError as ex:
                    print("<Service not available>")
                    
        except Exception as ex:
            print(ex)
            
        finally:
            # Disconnect
            try:
                if conn:
                    print("<Disconnect>")
                    conn.disconnect()
                    
            except Exception as ex:
                print(ex)

        gc.collect()
        print("gc.mem_free:", gc.mem_free())
        
    except Exception as ex:
        print(ex)

    time.sleep(1)

Sample Run Output:

code.py output:
<Connect> <Disconnect>
gc.mem_free: 116976
<Connect> <Disconnect>
gc.mem_free: 116096
<Connect> <Disconnect>
gc.mem_free: 115200
<Connect> <Disconnect>
gc.mem_free: 114320
<Connect> <Disconnect>
gc.mem_free: 113424
<Connect> <Disconnect>
gc.mem_free: 112544
<Connect> <Disconnect>
gc.mem_free: 111648

.
.
.

<Connect> <Disconnect>
gc.mem_free: 5088
<Connect> <Disconnect>
gc.mem_free: 3888
<Connect> <Disconnect>
gc.mem_free: 3008
<Connect> <Disconnect>
gc.mem_free: 2128
<Connect> <Disconnect>
gc.mem_free: 1248
<Connect> <Disconnect>
gc.mem_free: 368
<Connect> 
<Disconnect>
gc.mem_free: 32

This sample run ended with the device freezing and requiring a hardware reset. The elapsed time from the beginning of the run to the end was approximately 7 minutes and 45 seconds.

@300bps
Copy link
Author

300bps commented Dec 11, 2021

After a bit of looking at the source for the adafruit_ble library, I think I may see the cause of the issue that I described above.

Cause:
In adafruit_ble.__init__.py , line 291 in the 'connect' method of the BLERadio class adds each connection to the self._connection_cache dictionary, but entries are never removed (such as after they are disconnected). Line 306 of the 'connections' method does the same thing.

After each connect/disconnect, the _connection_cache dictionary grows by an entry. Since these entries are never cleared, a reference is retained for all of these connections which prevents the objects and all of the memory they hold references to from being garbage collected. In this example, all of these connections are closed and could/should be discarded (arguably).

This can be observed by using the code from my first post above by adding the following line after the 'print("gc.mem_free:", gc.mem_free())' line.

        print(ble._connection_cache)

Sample Run Output

code.py output:
<Connect> <Disconnect>
gc.mem_free: 116976
{<Connection>: <BLEConnection object at 0x20016fe0>}
<Connect> <Disconnect>
gc.mem_free: 116096
{<Connection>: <BLEConnection object at 0x20016fe0>, <Connection>: <BLEConnection object at 0x20016e60>}
<Connect> <Disconnect>
gc.mem_free: 115200
{<Connection>: <BLEConnection object at 0x20016fe0>, <Connection>: <BLEConnection object at 0x20016e60>, <Connection>: <BLEConnection object at 0x20017180>}
<Connect> <Disconnect>
gc.mem_free: 114320
{<Connection>: <BLEConnection object at 0x20016fe0>, <Connection>: <BLEConnection object at 0x20016e60>, <Connection>: <BLEConnection object at 0x20017180>, <Connection>: <BLEConnection object at 0x20016f10>}

As can be seen above, after just 4 connect/disconnects to the same peripheral, the '_connection_cache' dictionary holds 4 entries.

Possible improvements:
The following are some different ideas that come to mind that may improve on this behavior.

  1. While it would require some rework, perhaps caching connections using the BLE MAC address as the dictionary key may work. Repeated connect/disconnects to the same peripheral would only use a single entry and would allow the previously closed connections to that peripheral to be garbage collected. Perhaps desirably or undesirably though, closed connections to other/different peripherals (with different MACs) would still remain in the cache and their memory wouldn't be reclaimed.

  2. Store a reference to the BLERadio object in each connection so that the connection object's 'disconnect' method could call back into the associated BLERadio object to have it remove the connection from the _connection_cache after disconnect.

  3. Not sure how acceptable this would be, but the _connection_cache could be made a class variable of BLERadio and then the connection object could access it directly to have it perform the same action discussed in number 2.

  4. Put a limit on the number of items that will be cached to prevent memory exhaustion.

@dhalbert
Copy link
Collaborator

Closed via #147.

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 a pull request may close this issue.

2 participants