Skip to content

Commit 3346912

Browse files
committed
Make devices widget faster by batching some operations
1 parent 713cbc8 commit 3346912

File tree

1 file changed

+137
-65
lines changed

1 file changed

+137
-65
lines changed

qui/devices/device_widget.py

Lines changed: 137 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
get_fullscreen_window_hack,
2424
) # isort:skip
2525

26-
from typing import Set, List, Dict, Optional, Any
26+
from typing import Set, List, Dict, Optional
2727
import asyncio
2828
import sys
2929
import time
@@ -113,6 +113,8 @@ def __init__(self, app_name, qapp, dispatcher):
113113
self.dispvm_templates: Set[backend.VM] = set()
114114
self.parent_ports_to_hide = []
115115
self.sysusb: backend.VM | None = None
116+
self.dev_update_queue: Set = set()
117+
self.vm_update_queue: Set = set()
116118

117119
self.dispatcher: qubesadmin.events.EventsDispatcher = dispatcher
118120
self.qapp: qubesadmin.Qubes = qapp
@@ -132,7 +134,17 @@ def __init__(self, app_name, qapp, dispatcher):
132134
"device-detach:" + devclass, self.device_detached
133135
)
134136
self.dispatcher.add_handler(
135-
"device-list-change:" + devclass, self.device_list_update
137+
"device-list-change:" + devclass, self._update_queue
138+
)
139+
self.dispatcher.add_handler("device-added:" + devclass, self.device_added)
140+
self.dispatcher.add_handler(
141+
"device-removed:" + devclass, self.device_removed
142+
)
143+
self.dispatcher.add_handler(
144+
"device-assign:" + devclass, self.device_assigned
145+
)
146+
self.dispatcher.add_handler(
147+
"device-unassign:" + devclass, self.device_unassigned
136148
)
137149

138150
self.dispatcher.add_handler("domain-shutdown", self.vm_shutdown)
@@ -167,73 +179,55 @@ def __init__(self, app_name, qapp, dispatcher):
167179
"<b>Qubes Devices</b>\nView and manage devices."
168180
)
169181

170-
def device_list_update(self, vm, event, **_kwargs):
171-
devclass = event.split(":")[1]
172-
changed_devices: Dict[str, Any] = {}
173-
# create list of all current devices from the changed VM
174-
try:
175-
for device in vm.devices[devclass]:
176-
changed_devices[backend.Device.id_from_device(device)] = device
177-
except qubesadmin.exc.QubesException:
178-
changed_devices = {} # VM was removed
179-
180-
microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
181-
182-
for dev_id, device in changed_devices.items():
183-
if dev_id not in self.devices:
184-
dev = backend.Device(device, self)
185-
dev.connection_timestamp = time.monotonic()
186-
self.devices[dev_id] = dev
187-
if dev.parent:
188-
for potential_parent in self.devices.values():
189-
if potential_parent.port == dev.parent:
190-
potential_parent.has_children = True
191-
192-
# connect with mic
193-
mic_feature = vm.features.get(
194-
backend.FEATURE_ATTACH_WITH_MIC, ""
195-
).split(" ")
196-
if dev_id in mic_feature:
197-
microphone.devices_to_attach_with_me.append(dev)
198-
dev.devices_to_attach_with_me = [microphone]
182+
def _update_queue(self, vm, device, **_kwargs):
183+
"""Handle certain operations that should not be done too often."""
184+
# update children
185+
if vm not in self.vm_update_queue:
186+
self.vm_update_queue.add(vm)
187+
asyncio.create_task(self.update_parents(vm))
188+
if device not in self.dev_update_queue:
189+
self.dev_update_queue.add(device)
190+
asyncio.create_task(self.update_assignments())
191+
192+
async def update_assignments(self):
193+
"""Scan vm list for new assignments"""
194+
await asyncio.sleep(0.3)
195+
196+
if not self.dev_update_queue:
197+
return
198+
devs = self.dev_update_queue.copy()
199+
self.dev_update_queue.clear()
199200

200-
# hide children
201-
child_feature = vm.features.get(
202-
backend.FEATURE_HIDE_CHILDREN, ""
203-
).split(" ")
204-
if dev_id in child_feature:
205-
self.parent_ports_to_hide.append(dev.port)
206-
dev.show_children = False
201+
for domain in self.qapp.domains:
202+
for devclass in DEV_TYPES:
203+
try:
204+
for device in domain.devices[devclass].get_attached_devices():
205+
dev = backend.Device.id_from_device(device)
206+
if dev in devs and dev in self.devices:
207+
self.devices[dev].attachments.add(backend.VM(domain))
207208

208-
self.emit_notification(
209-
_("Device available"),
210-
_("Device {} is available.").format(dev.description),
211-
Gio.NotificationPriority.NORMAL,
212-
notification_id=dev.notification_id,
213-
)
209+
for device in domain.devices[devclass].get_assigned_devices():
210+
dev = backend.Device.id_from_device(device)
211+
if dev in devs and dev in self.devices:
212+
self.devices[dev].assignments.add(backend.VM(domain))
213+
except qubesadmin.exc.QubesException:
214+
# we have no permission to access VM's devices
215+
continue
214216

215-
dev_to_remove = []
217+
async def update_parents(self, vm):
218+
await asyncio.sleep(0.3)
216219

217-
for dev_id, dev in self.devices.items():
218-
if dev.backend_domain != vm or dev.device_class != devclass:
219-
continue
220-
if dev_id not in changed_devices:
221-
dev_to_remove.append((dev_id, dev))
222-
223-
for dev_id, dev in dev_to_remove:
224-
self.emit_notification(
225-
_("Device removed"),
226-
_("Device {} has been removed.").format(dev.description),
227-
Gio.NotificationPriority.NORMAL,
228-
notification_id=dev.notification_id,
229-
)
230-
if dev in microphone.devices_to_attach_with_me:
231-
microphone.devices_to_attach_with_me.remove(dev)
232-
if dev.port in self.parent_ports_to_hide:
233-
self.parent_ports_to_hide.remove(dev.port)
234-
del self.devices[dev_id]
235-
236-
self.hide_child_devices()
220+
if vm not in self.vm_update_queue:
221+
return
222+
self.vm_update_queue.remove(vm)
223+
224+
self.update_single_feature(
225+
None,
226+
None,
227+
backend.FEATURE_HIDE_CHILDREN,
228+
value=vm.features.get(backend.FEATURE_HIDE_CHILDREN, ""),
229+
oldvalue="",
230+
)
237231

238232
def initialize_vm_data(self):
239233
for vm in self.qapp.domains:
@@ -250,6 +244,61 @@ def initialize_vm_data(self):
250244
# we don't have access to VM state
251245
pass
252246

247+
def device_added(self, vm, _event, device):
248+
dev_id = backend.Device.id_from_device(device)
249+
dev = backend.Device(device, self)
250+
dev.connection_timestamp = time.monotonic()
251+
self.devices[dev_id] = dev
252+
253+
if dev.parent:
254+
for potential_parent in self.devices.values():
255+
if potential_parent.port == dev.parent:
256+
potential_parent.has_children = True
257+
break
258+
259+
# connect with mic
260+
mic_feature = vm.features.get(backend.FEATURE_ATTACH_WITH_MIC, "").split(" ")
261+
if dev_id in mic_feature:
262+
microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
263+
microphone.devices_to_attach_with_me.append(dev)
264+
dev.devices_to_attach_with_me = [microphone]
265+
266+
self.emit_notification(
267+
_("Device available"),
268+
_("Device {} is available.").format(dev.description),
269+
Gio.NotificationPriority.NORMAL,
270+
notification_id=dev.notification_id,
271+
)
272+
273+
self._update_queue(vm, dev_id)
274+
275+
def device_removed(self, vm, _event, port):
276+
for potential_dev_id, potential_dev in self.devices.items():
277+
if (
278+
potential_dev.backend_domain.name != vm.name
279+
or potential_dev.port != str(port)
280+
):
281+
continue
282+
dev, dev_id = potential_dev, potential_dev_id
283+
break
284+
else:
285+
# we never knew the device anyway
286+
return
287+
288+
microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
289+
290+
self.emit_notification(
291+
_("Device removed"),
292+
_("Device {} has been removed.").format(dev.description),
293+
Gio.NotificationPriority.NORMAL,
294+
notification_id=dev.notification_id,
295+
)
296+
if dev in microphone.devices_to_attach_with_me:
297+
microphone.devices_to_attach_with_me.remove(dev)
298+
if dev.port in self.parent_ports_to_hide:
299+
self.parent_ports_to_hide.remove(dev.port)
300+
del self.devices[dev_id]
301+
253302
def initialize_dev_data(self):
254303
# list all devices
255304
for domain in self.qapp.domains:
@@ -289,6 +338,23 @@ def initialize_dev_data(self):
289338
# we have no permission to access VM's devices
290339
continue
291340

341+
def device_assigned(self, vm, _event, device, **_kwargs):
342+
dev_id = backend.Device.id_from_device(device)
343+
if dev_id not in self.devices:
344+
return
345+
self.devices[dev_id].assignments.add(backend.VM(vm))
346+
347+
def device_unassigned(self, vm, _event, device, **_kwargs):
348+
dev_id = backend.Device.id_from_device(device)
349+
if dev_id not in self.devices:
350+
return
351+
try:
352+
self.devices[dev_id].assignments.remove(backend.VM(vm))
353+
except KeyError:
354+
# it's ok, somehow we got an unassign for a device we didn't store as
355+
# assigned. Cheers!
356+
return
357+
292358
def update_single_feature(self, _vm, _event, feature, value=None, oldvalue=None):
293359
if not value:
294360
new = set()
@@ -448,8 +514,14 @@ def vm_shutdown(self, vm, _event, **_kwargs):
448514
self.vms.discard(wrapped_vm)
449515
self.dispvm_templates.discard(wrapped_vm)
450516

517+
devs_to_remove = []
451518
for dev in self.devices.values():
519+
if dev.backend_domain == wrapped_vm:
520+
devs_to_remove.append(dev.port)
521+
continue
452522
dev.attachments.discard(wrapped_vm)
523+
for dev_port in devs_to_remove:
524+
self.device_removed(vm, None, port=dev_port)
453525

454526
def vm_dispvm_template_change(self, vm, _event, **_kwargs):
455527
"""Is template for dispvms property changed"""

0 commit comments

Comments
 (0)