perf(bluetooth): reduce CPU usage and improve async performance

This commit is contained in:
Michael Aaron Murphy 2025-04-15 15:43:24 +02:00 committed by Michael Murphy
parent b3515bb9ba
commit b19aea769a
4 changed files with 113 additions and 101 deletions

View file

@ -34,6 +34,7 @@ use crate::{
static BLUETOOTH_ENABLED: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique); static BLUETOOTH_ENABLED: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
#[inline]
pub fn run() -> cosmic::iced::Result { pub fn run() -> cosmic::iced::Result {
cosmic::applet::run::<CosmicBluetoothApplet>(()) cosmic::applet::run::<CosmicBluetoothApplet>(())
} }
@ -53,6 +54,7 @@ struct CosmicBluetoothApplet {
} }
impl CosmicBluetoothApplet { impl CosmicBluetoothApplet {
#[inline]
fn update_icon(&mut self) { fn update_icon(&mut self) {
self.icon_name = if self.bluer_state.bluetooth_enabled { self.icon_name = if self.bluer_state.bluetooth_enabled {
"cosmic-applet-bluetooth-active-symbolic" "cosmic-applet-bluetooth-active-symbolic"
@ -117,7 +119,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
self.popup.replace(new_id); self.popup.replace(new_id);
self.timeline = Timeline::new(); self.timeline = Timeline::new();
let mut popup_settings = self.core.applet.get_popup_settings( let popup_settings = self.core.applet.get_popup_settings(
self.core.main_window_id().unwrap(), self.core.main_window_id().unwrap(),
new_id, new_id,
None, None,
@ -348,6 +350,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
} = theme::active().cosmic().spacing; } = theme::active().cosmic().spacing;
let mut known_bluetooth = vec![]; let mut known_bluetooth = vec![];
// PERF: This should be pre-filtered in an update.
for dev in self.bluer_state.devices.iter().filter(|d| { for dev in self.bluer_state.devices.iter().filter(|d| {
!self !self
.request_confirmation .request_confirmation
@ -355,7 +358,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
.map_or(false, |(dev, _, _)| d.address == dev.address) .map_or(false, |(dev, _, _)| d.address == dev.address)
}) { }) {
let mut row = row![ let mut row = row![
icon::from_name(dev.icon.as_str()).size(16).symbolic(true), icon::from_name(dev.icon).size(16).symbolic(true),
text::body(dev.name.clone()) text::body(dev.name.clone())
.align_x(Alignment::Start) .align_x(Alignment::Start)
.align_y(Alignment::Center) .align_y(Alignment::Center)
@ -364,12 +367,8 @@ impl cosmic::Application for CosmicBluetoothApplet {
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(12); .spacing(12);
if let Some(DeviceProperty::BatteryPercentage(battery)) = dev if let Some(battery) = dev.battery_percent {
.properties let icon = match battery {
.iter()
.find(|p| matches!(p, DeviceProperty::BatteryPercentage(_)))
{
let icon = match *battery {
b if b >= 20 && b < 40 => "battery-low", b if b >= 20 && b < 40 => "battery-low",
b if b < 20 => "battery-caution", b if b < 20 => "battery-caution",
_ => "battery", _ => "battery",
@ -474,9 +473,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
if let Some((device, pin, _)) = self.request_confirmation.as_ref() { if let Some((device, pin, _)) = self.request_confirmation.as_ref() {
let row = column![ let row = column![
padded_control(row![ padded_control(row![
icon::from_name(device.icon.as_str()) icon::from_name(device.icon).size(16).symbolic(true),
.size(16)
.symbolic(true),
text::body(&device.name) text::body(&device.name)
.align_x(Alignment::Start) .align_x(Alignment::Start)
.align_y(Alignment::Center) .align_y(Alignment::Center)
@ -528,7 +525,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
&& (d.has_name() || d.is_known_device_type()) && (d.has_name() || d.is_known_device_type())
}) { }) {
let row = row![ let row = row![
icon::from_name(dev.icon.as_str()).size(16).symbolic(true), icon::from_name(dev.icon).size(16).symbolic(true),
text::body(dev.name.clone()).align_x(Alignment::Start), text::body(dev.name.clone()).align_x(Alignment::Start),
] ]
.align_y(Alignment::Center) .align_y(Alignment::Center)

View file

@ -18,6 +18,7 @@ use cosmic::{
iced_futures::stream, iced_futures::stream,
}; };
use futures::{stream::FuturesUnordered, FutureExt};
use rand::Rng; use rand::Rng;
use tokio::{ use tokio::{
spawn, spawn,
@ -28,8 +29,8 @@ use tokio::{
task::JoinHandle, task::JoinHandle,
time::timeout, time::timeout,
}; };
// Copied from https://github.com/bluez/bluez/blob/39467578207889fd015775cbe81a3db9dd26abea/src/dbus-common.c#L53 // Copied from https://github.com/bluez/bluez/blob/39467578207889fd015775cbe81a3db9dd26abea/src/dbus-common.c#L53
#[inline]
fn device_type_to_icon(device_type: &str) -> &'static str { fn device_type_to_icon(device_type: &str) -> &'static str {
match device_type { match device_type {
"computer" => "laptop-symbolic", "computer" => "laptop-symbolic",
@ -49,6 +50,7 @@ fn device_type_to_icon(device_type: &str) -> &'static str {
} }
} }
#[inline]
pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>( pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I, id: I,
) -> iced::Subscription<BluerEvent> { ) -> iced::Subscription<BluerEvent> {
@ -70,7 +72,7 @@ pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
.await; .await;
}; };
let state = session_state.bluer_state().await; let state = bluer_state(&session_state.adapter).await;
// reconnect to paired and trusted devices // reconnect to paired and trusted devices
if state.bluetooth_enabled { if state.bluetooth_enabled {
@ -90,29 +92,28 @@ pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
}) })
.await; .await;
let mut event_handler = async |event| match event { let mut event_handler = async |event| {
BluerSessionEvent::ChangesProcessed(state) => { let message = match event {
_ = output.send(BluerEvent::DevicesChanged { state }).await; BluerSessionEvent::ChangesProcessed(state) => {
} BluerEvent::DevicesChanged { state }
}
BluerSessionEvent::RequestResponse { BluerSessionEvent::RequestResponse {
req, req,
state, state,
err_msg, err_msg,
} => { } => BluerEvent::RequestResponse {
_ = output req,
.send(BluerEvent::RequestResponse { state,
req, err_msg,
state, },
err_msg,
})
.await;
}
BluerSessionEvent::AgentEvent(e) => {
_ = output.send(BluerEvent::AgentEvent(e)).await;
}
_ => {} BluerSessionEvent::AgentEvent(e) => BluerEvent::AgentEvent(e),
_ => return,
};
_ = output.send(message).await;
}; };
let mut interval = tokio::time::interval(Duration::from_secs(1)); let mut interval = tokio::time::interval(Duration::from_secs(1));
@ -124,6 +125,7 @@ pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
if let Some(event) = session_rx.recv().await { if let Some(event) = session_rx.recv().await {
event_handler(event).await; event_handler(event).await;
// Consume any additional available events.
while let Some(event) = session_rx.try_recv().ok() { while let Some(event) = session_rx.try_recv().ok() {
event_handler(event).await; event_handler(event).await;
} }
@ -190,15 +192,18 @@ pub enum BluerDeviceStatus {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BluerDevice { pub struct BluerDevice {
pub name: String, pub name: String,
pub icon: &'static str,
pub address: Address, pub address: Address,
pub status: BluerDeviceStatus, pub status: BluerDeviceStatus,
pub properties: Vec<DeviceProperty>, pub battery_percent: Option<u8>,
pub icon: String, pub is_paired: bool,
pub is_trusted: bool,
} }
impl Eq for BluerDevice {} impl Eq for BluerDevice {}
impl Ord for BluerDevice { impl Ord for BluerDevice {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering { fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.status.cmp(&other.status) { match self.status.cmp(&other.status) {
std::cmp::Ordering::Equal => self.name.to_lowercase().cmp(&other.name.to_lowercase()), std::cmp::Ordering::Equal => self.name.to_lowercase().cmp(&other.name.to_lowercase()),
@ -208,6 +213,7 @@ impl Ord for BluerDevice {
} }
impl PartialOrd for BluerDevice { impl PartialOrd for BluerDevice {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match self.status.cmp(&other.status) { match self.status.cmp(&other.status) {
std::cmp::Ordering::Equal => { std::cmp::Ordering::Equal => {
@ -219,6 +225,7 @@ impl PartialOrd for BluerDevice {
} }
impl PartialEq for BluerDevice { impl PartialEq for BluerDevice {
#[inline]
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.address == other.address self.name == other.name && self.address == other.address
} }
@ -227,18 +234,26 @@ impl PartialEq for BluerDevice {
const DEFAULT_DEVICE_ICON: &str = "bluetooth-symbolic"; const DEFAULT_DEVICE_ICON: &str = "bluetooth-symbolic";
impl BluerDevice { impl BluerDevice {
#[inline(never)]
pub async fn from_device(device: &bluer::Device) -> Self { pub async fn from_device(device: &bluer::Device) -> Self {
let mut name = device let (mut name, is_paired, is_trusted, is_connected, battery_percent, icon) = futures::join!(
.name() device.name().map(|res| res
.await .ok()
.unwrap_or_default() .flatten()
.unwrap_or_else(|| device.address().to_string()); .unwrap_or_else(|| device.address().to_string())),
device.is_paired().map(Result::unwrap_or_default),
device.is_trusted().map(Result::unwrap_or_default),
device.is_connected().map(Result::unwrap_or_default),
device.battery_percentage().map(|res| res.ok().flatten()),
device
.icon()
.map(|res| device_type_to_icon(&res.ok().flatten().unwrap_or_default()))
);
if name.is_empty() { if name.is_empty() {
name = device.address().to_string(); name = device.address().to_string();
}; };
let is_paired = device.is_paired().await.unwrap_or_default();
let is_connected = device.is_connected().await.unwrap_or_default();
let properties = device.all_properties().await.unwrap_or_default();
let status = if is_connected { let status = if is_connected {
BluerDeviceStatus::Connected BluerDeviceStatus::Connected
} else if is_paired { } else if is_paired {
@ -246,44 +261,30 @@ impl BluerDevice {
} else { } else {
BluerDeviceStatus::Disconnected BluerDeviceStatus::Disconnected
}; };
let icon = properties
.iter()
.find_map(|p| {
if let DeviceProperty::Icon(icon) = p {
Some(device_type_to_icon(icon.clone().as_str()).to_string())
} else {
None
}
})
.unwrap_or_else(|| device_type_to_icon(DEFAULT_DEVICE_ICON).to_string());
Self { Self {
name, name,
icon,
address: device.address(), address: device.address(),
status, status,
properties, battery_percent,
icon, is_paired,
is_trusted,
} }
} }
#[inline]
fn paired_and_trusted(&self) -> bool { fn paired_and_trusted(&self) -> bool {
self.properties self.is_paired && self.is_trusted
.iter()
.filter(|p| {
matches!(
p,
DeviceProperty::Trusted(true) | DeviceProperty::Paired(true)
)
})
.count()
== 2
} }
#[inline]
#[must_use] #[must_use]
pub fn is_known_device_type(&self) -> bool { pub fn is_known_device_type(&self) -> bool {
self.icon != DEFAULT_DEVICE_ICON self.icon != DEFAULT_DEVICE_ICON
} }
#[inline]
#[must_use] #[must_use]
pub fn has_name(&self) -> bool { pub fn has_name(&self) -> bool {
self.name != self.address.to_string() self.name != self.address.to_string()
@ -326,9 +327,8 @@ pub struct BluerSessionState {
} }
impl BluerSessionState { impl BluerSessionState {
pub(crate) async fn new(session: Session) -> anyhow::Result<Self> { async fn new(session: Session) -> anyhow::Result<Self> {
let adapter = session.default_adapter().await?; let adapter = session.default_adapter().await?;
let devices = build_device_list(&adapter).await;
let (tx, rx) = tokio::sync::mpsc::channel(100); let (tx, rx) = tokio::sync::mpsc::channel(100);
let (req_tx, req_rx) = channel(100); let (req_tx, req_rx) = channel(100);
let tx_clone_1 = tx.clone(); let tx_clone_1 = tx.clone();
@ -524,6 +524,7 @@ impl BluerSessionState {
Ok(self_) Ok(self_)
} }
#[inline]
fn listen_bluetooth_power_changes(&self) { fn listen_bluetooth_power_changes(&self) {
let tx = self.tx.clone(); let tx = self.tx.clone();
let req_tx = self.req_tx.clone(); let req_tx = self.req_tx.clone();
@ -532,13 +533,15 @@ impl BluerSessionState {
let _handle: JoinHandle<anyhow::Result<()>> = spawn(async move { let _handle: JoinHandle<anyhow::Result<()>> = spawn(async move {
let mut status = adapter_clone.is_powered().await.unwrap_or_default(); let mut status = adapter_clone.is_powered().await.unwrap_or_default();
let mut interval = tokio::time::interval(Duration::from_secs(3)); let mut interval = tokio::time::interval(Duration::from_secs(3));
let mut devices = Vec::new();
loop { loop {
interval.tick().await; interval.tick().await;
let new_status = adapter_clone.is_powered().await.unwrap_or_default(); let new_status = adapter_clone.is_powered().await.unwrap_or_default();
devices = build_device_list(devices, &adapter_clone).await;
if new_status != status { if new_status != status {
status = new_status; status = new_status;
let state = BluerState { let state = BluerState {
devices: build_device_list(&adapter_clone).await, devices: devices.clone(),
bluetooth_enabled: status, bluetooth_enabled: status,
}; };
if state.bluetooth_enabled { if state.bluetooth_enabled {
@ -555,7 +558,8 @@ impl BluerSessionState {
}); });
} }
pub(crate) fn process_changes(&mut self) { #[inline]
fn process_changes(&mut self) {
let tx = self.tx.clone(); let tx = self.tx.clone();
let req_tx = self.req_tx.clone(); let req_tx = self.req_tx.clone();
let Some(mut wake_up) = self.wake_up_discover_rx.take() else { let Some(mut wake_up) = self.wake_up_discover_rx.take() else {
@ -565,8 +569,8 @@ impl BluerSessionState {
let adapter_clone = self.adapter.clone(); let adapter_clone = self.adapter.clone();
let _monitor_devices: tokio::task::JoinHandle<Result<(), anyhow::Error>> = spawn( let _monitor_devices: tokio::task::JoinHandle<Result<(), anyhow::Error>> = spawn(
async move { async move {
let mut milli_timeout = 10;
let mut change_stream = { let mut change_stream = {
let mut milli_timeout = 10;
let mut res = adapter_clone.discover_devices_with_changes().await; let mut res = adapter_clone.discover_devices_with_changes().await;
while res.is_err() { while res.is_err() {
_ = tokio::time::timeout( _ = tokio::time::timeout(
@ -577,14 +581,15 @@ impl BluerSessionState {
res = adapter_clone.discover_devices_with_changes().await; res = adapter_clone.discover_devices_with_changes().await;
milli_timeout = milli_timeout.saturating_mul(5); milli_timeout = milli_timeout.saturating_mul(5);
} }
milli_timeout = 10;
res.unwrap() res.unwrap()
}; };
let mut interval = tokio::time::interval(Duration::from_secs(1)); let mut interval = tokio::time::interval(Duration::from_secs(1));
loop { loop {
let mut milli_timeout = 10;
let mut devices: Vec<BluerDevice> = Vec::new(); let mut devices: Vec<BluerDevice> = Vec::new();
let mut new_devices = Vec::new();
'outer: loop { 'outer: loop {
tokio::select! { tokio::select! {
change = timeout(Duration::from_millis(milli_timeout), change_stream.next()) => { change = timeout(Duration::from_millis(milli_timeout), change_stream.next()) => {
@ -601,35 +606,34 @@ impl BluerSessionState {
} }
}; };
let mut new_devices = build_device_list(&adapter_clone).await; new_devices = build_device_list(new_devices, &adapter_clone).await;
for d in new_devices for d in new_devices
.iter() .iter()
.filter(|d| !devices.contains(d) && d.paired_and_trusted()) .filter(|d| !devices.contains(d) && d.paired_and_trusted())
{ {
_ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await; _ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await;
} }
devices = mem::take(&mut new_devices);
mem::swap(&mut new_devices, &mut devices);
let _ = tx let _ = tx
.send(BluerSessionEvent::ChangesProcessed(BluerState { .send(BluerSessionEvent::ChangesProcessed(BluerState {
devices: build_device_list(&adapter_clone).await, devices: devices.clone(),
bluetooth_enabled: adapter_clone bluetooth_enabled: adapter_clone
.is_powered() .is_powered()
.await .await
.unwrap_or_default(), .unwrap_or_default(),
})) }))
.await; .await;
// reset timeout interval.tick().await;
milli_timeout = 10;
} }
let _ = tx.send(BluerSessionEvent::ChangeStreamEnded).await; let _ = tx.send(BluerSessionEvent::ChangeStreamEnded).await;
interval.tick().await;
} }
}, },
); );
} }
pub(crate) fn process_requests(&self, request_rx: Receiver<BluerRequest>) { #[inline]
fn process_requests(&self, request_rx: Receiver<BluerRequest>) {
let active_requests = self.active_requests.clone(); let active_requests = self.active_requests.clone();
let adapter = self.adapter.clone(); let adapter = self.adapter.clone();
let tx = self.tx.clone(); let tx = self.tx.clone();
@ -740,15 +744,10 @@ impl BluerSessionState {
BluerRequest::StateUpdate => {} BluerRequest::StateUpdate => {}
}; };
let state = BluerState {
devices: build_device_list(&adapter_clone).await,
bluetooth_enabled: adapter_clone.is_powered().await.unwrap_or_default(),
};
let _ = tx_clone let _ = tx_clone
.send(BluerSessionEvent::RequestResponse { .send(BluerSessionEvent::RequestResponse {
req: req_clone, req: req_clone,
state, state: bluer_state(&adapter_clone).await,
err_msg, err_msg,
}) })
.await; .await;
@ -763,28 +762,42 @@ impl BluerSessionState {
Ok(()) Ok(())
}); });
} }
}
pub(crate) async fn bluer_state(&self) -> BluerState { #[inline]
BluerState { async fn bluer_state(adapter: &Adapter) -> BluerState {
devices: build_device_list(&self.adapter).await, let (devices, bluetooth_enabled) = futures::join!(
// TODO is this a proper way of checking if bluetooth is enabled? build_device_list(Vec::new(), adapter),
bluetooth_enabled: self.adapter.is_powered().await.unwrap_or_default(), // TODO is this a proper way of checking if bluetooth is enabled?
} adapter.is_powered().map(Result::unwrap_or_default),
);
BluerState {
devices,
bluetooth_enabled,
} }
} }
async fn build_device_list(adapter: &Adapter) -> Vec<BluerDevice> { #[inline(never)]
async fn build_device_list(mut devices: Vec<BluerDevice>, adapter: &Adapter) -> Vec<BluerDevice> {
let addrs = adapter.device_addresses().await.unwrap_or_default(); let addrs = adapter.device_addresses().await.unwrap_or_default();
let mut devices = Vec::with_capacity(addrs.len());
for address in addrs { devices.clear();
let device = match adapter.device(address) { if addrs.len() > devices.capacity() {
Ok(device) => device, devices.reserve(addrs.len() - devices.capacity());
Err(_) => continue,
};
devices.push(BluerDevice::from_device(&device).await);
} }
// Concurrently collect bluer devices from each address.
let mut device_stream = addrs
.into_iter()
.filter_map(|address| adapter.device(address).ok())
.map(async move |device| BluerDevice::from_device(&device).await)
.collect::<FuturesUnordered<_>>();
while let Some(device) = device_stream.next().await {
devices.push(device)
}
devices.sort(); devices.sort();
devices devices
} }

View file

@ -8,6 +8,7 @@ mod localize;
use crate::localize::localize; use crate::localize::localize;
#[inline]
pub fn run() -> cosmic::iced::Result { pub fn run() -> cosmic::iced::Result {
localize(); localize();
app::run() app::run()

View file

@ -34,6 +34,7 @@ macro_rules! fl {
} }
// Get the `Localizer` to be used for localizing this library. // Get the `Localizer` to be used for localizing this library.
#[inline]
pub fn localizer() -> Box<dyn Localizer> { pub fn localizer() -> Box<dyn Localizer> {
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
} }