Skip to content

Repeated use of BLE discovery constantly increases threads used by application #83

@andrewdavidmackenzie

Description

@andrewdavidmackenzie

Describe the bug
Today, due to some jankyness in my usually smooth laptop performance, I looked at ActivityMonitor in macOS and my Iced/Tokio app that uses meshtastic/rust crate had 1400 threads running.

Upon investigation it looks like my iced subscription (which scans for BLE devices every 4 seconds) is creating a thread or two every iteration...and they never die, leading to the number of threads used by the application growing constantly.

Thread count starts around 35 and increases 1 or 2 every 4 seconds, the period of this iteration in my code:

pub fn ble_discovery() -> impl Stream<Item = DiscoveryEvent> {
    stream::channel(100, move |mut gui_sender| async move {
        let mut mesh_radio_ids: Vec<BleDevice> = vec![];

        // loop scanning for devices
        loop {
            match available_ble_devices(Duration::from_secs(4)).await {
                Ok(radios_now_ids) => {
                    // detect lost radios
                    for id in &mesh_radio_ids {
                        if !radios_now_ids.iter().any(|other_id| id == other_id) {
                            // inform GUI of a device lost
                            gui_sender
                                .send(BLERadioLost(id.clone()))
                                .await
                                .unwrap_or_else(|e| eprintln!("Discovery gui send error: {e}"));
                        }
                    }

                    // detect new radios found
                    for id in &radios_now_ids {
                        if !mesh_radio_ids.iter().any(|other_id| id == other_id) {
                            // track it for the future
                            mesh_radio_ids.push(id.clone());

                            // inform GUI of a new device found
                            gui_sender
                                .send(BLERadioFound(id.clone()))
                                .await
                                .unwrap_or_else(|e| eprintln!("Discovery gui send error: {e}"));
                        }
                    }
                }
                Err(e) => {
                    ...elided...
                }
            }
        }
    })
}

Digging into available_ble_devices code it looks like Àdapter::newcallsrun_corebluetooth_thread` and that seems to create threads that never die.

Call Graph

available_ble_devices
--> BleHandler::available_ble_devices
--> available_peripherals
--> Manager.adapters
--> Adapter::new
--> `run_corebluetooth_thread'

pub fn run_corebluetooth_thread(
    event_sender: Sender<CoreBluetoothEvent>,
) -> Result<Sender<CoreBluetoothMessage>, Error> {
    let authorization = unsafe { CBManager::authorization_class() };
    if authorization != CBManagerAuthorization::AllowedAlways
        && authorization != CBManagerAuthorization::NotDetermined
    {
        warn!("Authorization status {:?}", authorization);
        return Err(Error::PermissionDenied);
    } else {
        trace!("Authorization status {:?}", authorization);
    }
    let (sender, receiver) = mpsc::channel::<CoreBluetoothMessage>(256);
    // CoreBluetoothInternal is !Send, so we need to keep it on a single thread.
    thread::spawn(move || {
        let runtime = runtime::Builder::new_current_thread().build().unwrap();
        runtime.block_on(async move {
            let mut cbi = CoreBluetoothInternal::new(receiver, event_sender);
            loop {
                cbi.wait_for_message().await;
            }
        })
    });
    Ok(sender)
}

that seems to loop forever in the thread?

loop {
                cbi.wait_for_message().await;
            }

Adapter::new() also spawn tasks of its own, but those may die on channel closure, I didn't check the detailed logic of that.

To Reproduce
Steps to reproduce:

  1. Have some code that performs discovery using available_ble_devices in a loop that never exits
  2. Observe the number of threads in ActivityMonitor (or equivalent) grow constantly

Expected behavior
Threads to die after a short time or when they cease to be useful, or being reused.

Additional context
Screenshot on app startup (37 threads)

Image

Screenshot after a short period (without use, just doing discovery) (251 threads)

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions