perf(bluetooth): reduce CPU usage and improve async performance
This commit is contained in:
parent
b3515bb9ba
commit
b19aea769a
4 changed files with 113 additions and 101 deletions
|
|
@ -34,6 +34,7 @@ use crate::{
|
|||
|
||||
static BLUETOOTH_ENABLED: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
|
||||
|
||||
#[inline]
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
cosmic::applet::run::<CosmicBluetoothApplet>(())
|
||||
}
|
||||
|
|
@ -53,6 +54,7 @@ struct CosmicBluetoothApplet {
|
|||
}
|
||||
|
||||
impl CosmicBluetoothApplet {
|
||||
#[inline]
|
||||
fn update_icon(&mut self) {
|
||||
self.icon_name = if self.bluer_state.bluetooth_enabled {
|
||||
"cosmic-applet-bluetooth-active-symbolic"
|
||||
|
|
@ -117,7 +119,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
|
|||
self.popup.replace(new_id);
|
||||
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(),
|
||||
new_id,
|
||||
None,
|
||||
|
|
@ -348,6 +350,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
|
|||
} = theme::active().cosmic().spacing;
|
||||
|
||||
let mut known_bluetooth = vec![];
|
||||
// PERF: This should be pre-filtered in an update.
|
||||
for dev in self.bluer_state.devices.iter().filter(|d| {
|
||||
!self
|
||||
.request_confirmation
|
||||
|
|
@ -355,7 +358,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
|
|||
.map_or(false, |(dev, _, _)| d.address == dev.address)
|
||||
}) {
|
||||
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())
|
||||
.align_x(Alignment::Start)
|
||||
.align_y(Alignment::Center)
|
||||
|
|
@ -364,12 +367,8 @@ impl cosmic::Application for CosmicBluetoothApplet {
|
|||
.align_y(Alignment::Center)
|
||||
.spacing(12);
|
||||
|
||||
if let Some(DeviceProperty::BatteryPercentage(battery)) = dev
|
||||
.properties
|
||||
.iter()
|
||||
.find(|p| matches!(p, DeviceProperty::BatteryPercentage(_)))
|
||||
{
|
||||
let icon = match *battery {
|
||||
if let Some(battery) = dev.battery_percent {
|
||||
let icon = match battery {
|
||||
b if b >= 20 && b < 40 => "battery-low",
|
||||
b if b < 20 => "battery-caution",
|
||||
_ => "battery",
|
||||
|
|
@ -474,9 +473,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
|
|||
if let Some((device, pin, _)) = self.request_confirmation.as_ref() {
|
||||
let row = column![
|
||||
padded_control(row![
|
||||
icon::from_name(device.icon.as_str())
|
||||
.size(16)
|
||||
.symbolic(true),
|
||||
icon::from_name(device.icon).size(16).symbolic(true),
|
||||
text::body(&device.name)
|
||||
.align_x(Alignment::Start)
|
||||
.align_y(Alignment::Center)
|
||||
|
|
@ -528,7 +525,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
|
|||
&& (d.has_name() || d.is_known_device_type())
|
||||
}) {
|
||||
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),
|
||||
]
|
||||
.align_y(Alignment::Center)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use cosmic::{
|
|||
iced_futures::stream,
|
||||
};
|
||||
|
||||
use futures::{stream::FuturesUnordered, FutureExt};
|
||||
use rand::Rng;
|
||||
use tokio::{
|
||||
spawn,
|
||||
|
|
@ -28,8 +29,8 @@ use tokio::{
|
|||
task::JoinHandle,
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
// 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 {
|
||||
match device_type {
|
||||
"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>(
|
||||
id: I,
|
||||
) -> iced::Subscription<BluerEvent> {
|
||||
|
|
@ -70,7 +72,7 @@ pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
|||
.await;
|
||||
};
|
||||
|
||||
let state = session_state.bluer_state().await;
|
||||
let state = bluer_state(&session_state.adapter).await;
|
||||
|
||||
// reconnect to paired and trusted devices
|
||||
if state.bluetooth_enabled {
|
||||
|
|
@ -90,29 +92,28 @@ pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
|||
})
|
||||
.await;
|
||||
|
||||
let mut event_handler = async |event| match event {
|
||||
BluerSessionEvent::ChangesProcessed(state) => {
|
||||
_ = output.send(BluerEvent::DevicesChanged { state }).await;
|
||||
}
|
||||
let mut event_handler = async |event| {
|
||||
let message = match event {
|
||||
BluerSessionEvent::ChangesProcessed(state) => {
|
||||
BluerEvent::DevicesChanged { state }
|
||||
}
|
||||
|
||||
BluerSessionEvent::RequestResponse {
|
||||
req,
|
||||
state,
|
||||
err_msg,
|
||||
} => {
|
||||
_ = output
|
||||
.send(BluerEvent::RequestResponse {
|
||||
req,
|
||||
state,
|
||||
err_msg,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
BluerSessionEvent::AgentEvent(e) => {
|
||||
_ = output.send(BluerEvent::AgentEvent(e)).await;
|
||||
}
|
||||
BluerSessionEvent::RequestResponse {
|
||||
req,
|
||||
state,
|
||||
err_msg,
|
||||
} => BluerEvent::RequestResponse {
|
||||
req,
|
||||
state,
|
||||
err_msg,
|
||||
},
|
||||
|
||||
_ => {}
|
||||
BluerSessionEvent::AgentEvent(e) => BluerEvent::AgentEvent(e),
|
||||
|
||||
_ => return,
|
||||
};
|
||||
|
||||
_ = output.send(message).await;
|
||||
};
|
||||
|
||||
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 {
|
||||
event_handler(event).await;
|
||||
// Consume any additional available events.
|
||||
while let Some(event) = session_rx.try_recv().ok() {
|
||||
event_handler(event).await;
|
||||
}
|
||||
|
|
@ -190,15 +192,18 @@ pub enum BluerDeviceStatus {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct BluerDevice {
|
||||
pub name: String,
|
||||
pub icon: &'static str,
|
||||
pub address: Address,
|
||||
pub status: BluerDeviceStatus,
|
||||
pub properties: Vec<DeviceProperty>,
|
||||
pub icon: String,
|
||||
pub battery_percent: Option<u8>,
|
||||
pub is_paired: bool,
|
||||
pub is_trusted: bool,
|
||||
}
|
||||
|
||||
impl Eq for BluerDevice {}
|
||||
|
||||
impl Ord for BluerDevice {
|
||||
#[inline]
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.status.cmp(&other.status) {
|
||||
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 {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
match self.status.cmp(&other.status) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
|
|
@ -219,6 +225,7 @@ impl PartialOrd for BluerDevice {
|
|||
}
|
||||
|
||||
impl PartialEq for BluerDevice {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name && self.address == other.address
|
||||
}
|
||||
|
|
@ -227,18 +234,26 @@ impl PartialEq for BluerDevice {
|
|||
const DEFAULT_DEVICE_ICON: &str = "bluetooth-symbolic";
|
||||
|
||||
impl BluerDevice {
|
||||
#[inline(never)]
|
||||
pub async fn from_device(device: &bluer::Device) -> Self {
|
||||
let mut name = device
|
||||
.name()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_else(|| device.address().to_string());
|
||||
let (mut name, is_paired, is_trusted, is_connected, battery_percent, icon) = futures::join!(
|
||||
device.name().map(|res| res
|
||||
.ok()
|
||||
.flatten()
|
||||
.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() {
|
||||
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 {
|
||||
BluerDeviceStatus::Connected
|
||||
} else if is_paired {
|
||||
|
|
@ -246,44 +261,30 @@ impl BluerDevice {
|
|||
} else {
|
||||
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 {
|
||||
name,
|
||||
icon,
|
||||
address: device.address(),
|
||||
status,
|
||||
properties,
|
||||
icon,
|
||||
battery_percent,
|
||||
is_paired,
|
||||
is_trusted,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn paired_and_trusted(&self) -> bool {
|
||||
self.properties
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
matches!(
|
||||
p,
|
||||
DeviceProperty::Trusted(true) | DeviceProperty::Paired(true)
|
||||
)
|
||||
})
|
||||
.count()
|
||||
== 2
|
||||
self.is_paired && self.is_trusted
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn is_known_device_type(&self) -> bool {
|
||||
self.icon != DEFAULT_DEVICE_ICON
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn has_name(&self) -> bool {
|
||||
self.name != self.address.to_string()
|
||||
|
|
@ -326,9 +327,8 @@ pub struct 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 devices = build_device_list(&adapter).await;
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(100);
|
||||
let (req_tx, req_rx) = channel(100);
|
||||
let tx_clone_1 = tx.clone();
|
||||
|
|
@ -524,6 +524,7 @@ impl BluerSessionState {
|
|||
Ok(self_)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn listen_bluetooth_power_changes(&self) {
|
||||
let tx = self.tx.clone();
|
||||
let req_tx = self.req_tx.clone();
|
||||
|
|
@ -532,13 +533,15 @@ impl BluerSessionState {
|
|||
let _handle: JoinHandle<anyhow::Result<()>> = spawn(async move {
|
||||
let mut status = adapter_clone.is_powered().await.unwrap_or_default();
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(3));
|
||||
let mut devices = Vec::new();
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let new_status = adapter_clone.is_powered().await.unwrap_or_default();
|
||||
devices = build_device_list(devices, &adapter_clone).await;
|
||||
if new_status != status {
|
||||
status = new_status;
|
||||
let state = BluerState {
|
||||
devices: build_device_list(&adapter_clone).await,
|
||||
devices: devices.clone(),
|
||||
bluetooth_enabled: status,
|
||||
};
|
||||
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 req_tx = self.req_tx.clone();
|
||||
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 _monitor_devices: tokio::task::JoinHandle<Result<(), anyhow::Error>> = spawn(
|
||||
async move {
|
||||
let mut milli_timeout = 10;
|
||||
let mut change_stream = {
|
||||
let mut milli_timeout = 10;
|
||||
let mut res = adapter_clone.discover_devices_with_changes().await;
|
||||
while res.is_err() {
|
||||
_ = tokio::time::timeout(
|
||||
|
|
@ -577,14 +581,15 @@ impl BluerSessionState {
|
|||
res = adapter_clone.discover_devices_with_changes().await;
|
||||
milli_timeout = milli_timeout.saturating_mul(5);
|
||||
}
|
||||
milli_timeout = 10;
|
||||
res.unwrap()
|
||||
};
|
||||
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(1));
|
||||
|
||||
loop {
|
||||
let mut milli_timeout = 10;
|
||||
let mut devices: Vec<BluerDevice> = Vec::new();
|
||||
let mut new_devices = Vec::new();
|
||||
'outer: loop {
|
||||
tokio::select! {
|
||||
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
|
||||
.iter()
|
||||
.filter(|d| !devices.contains(d) && d.paired_and_trusted())
|
||||
{
|
||||
_ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await;
|
||||
}
|
||||
devices = mem::take(&mut new_devices);
|
||||
|
||||
mem::swap(&mut new_devices, &mut devices);
|
||||
let _ = tx
|
||||
.send(BluerSessionEvent::ChangesProcessed(BluerState {
|
||||
devices: build_device_list(&adapter_clone).await,
|
||||
devices: devices.clone(),
|
||||
bluetooth_enabled: adapter_clone
|
||||
.is_powered()
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
}))
|
||||
.await;
|
||||
// reset timeout
|
||||
milli_timeout = 10;
|
||||
interval.tick().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 adapter = self.adapter.clone();
|
||||
let tx = self.tx.clone();
|
||||
|
|
@ -740,15 +744,10 @@ impl BluerSessionState {
|
|||
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
|
||||
.send(BluerSessionEvent::RequestResponse {
|
||||
req: req_clone,
|
||||
state,
|
||||
state: bluer_state(&adapter_clone).await,
|
||||
err_msg,
|
||||
})
|
||||
.await;
|
||||
|
|
@ -763,28 +762,42 @@ impl BluerSessionState {
|
|||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn bluer_state(&self) -> BluerState {
|
||||
BluerState {
|
||||
devices: build_device_list(&self.adapter).await,
|
||||
// TODO is this a proper way of checking if bluetooth is enabled?
|
||||
bluetooth_enabled: self.adapter.is_powered().await.unwrap_or_default(),
|
||||
}
|
||||
#[inline]
|
||||
async fn bluer_state(adapter: &Adapter) -> BluerState {
|
||||
let (devices, bluetooth_enabled) = futures::join!(
|
||||
build_device_list(Vec::new(), adapter),
|
||||
// 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 mut devices = Vec::with_capacity(addrs.len());
|
||||
|
||||
for address in addrs {
|
||||
let device = match adapter.device(address) {
|
||||
Ok(device) => device,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
devices.push(BluerDevice::from_device(&device).await);
|
||||
devices.clear();
|
||||
if addrs.len() > devices.capacity() {
|
||||
devices.reserve(addrs.len() - devices.capacity());
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ mod localize;
|
|||
|
||||
use crate::localize::localize;
|
||||
|
||||
#[inline]
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize();
|
||||
app::run()
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ macro_rules! fl {
|
|||
}
|
||||
|
||||
// Get the `Localizer` to be used for localizing this library.
|
||||
#[inline]
|
||||
pub fn localizer() -> Box<dyn Localizer> {
|
||||
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue