From 3f528d665af935b8d990bfb709b0436efb92b117 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Tue, 9 Jun 2026 18:43:41 +0200 Subject: [PATCH 1/7] Skip case normalization for speed by matching case Each call was recording 1ms, now the calls are below that. --- qubes/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qubes/events.py b/qubes/events.py index a676652fb..5783ff6e3 100644 --- a/qubes/events.py +++ b/qubes/events.py @@ -160,7 +160,7 @@ def _fire_event(self, event, kwargs, pre_event=False): h_func for h_name, h_func_set in handlers_dict.items() for h_func in h_func_set - if fnmatch.fnmatch(event, h_name) + if fnmatch.fnmatchcase(event, h_name) ] for func in sorted( handlers, From a71f029cdae6832f56da874ef6da46d328640ca3 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 4 Jun 2026 07:56:01 +0200 Subject: [PATCH 2/7] Cache pure function to get entry points This function reimports the same information over and over and is time consuming when looping through multiple domains. Reduce listing USB devices while there is no cache from 2.8s to 0.5s. --- qubes/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qubes/utils.py b/qubes/utils.py index 73618f464..5a25a6a92 100644 --- a/qubes/utils.py +++ b/qubes/utils.py @@ -21,6 +21,7 @@ # import asyncio +import functools import hashlib import logging import re @@ -177,6 +178,7 @@ def urandom(size): return hashlib.sha512(rand).digest() +@functools.cache def get_entry_point_one(group, name): epoints = tuple(importlib.metadata.entry_points(group=group, name=name)) if not epoints: From d9e763447d62fbed34cfac4796ec4818ff0a687d Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 4 Jun 2026 09:31:04 +0200 Subject: [PATCH 3/7] Avoid use of startwith in a large loop Method 'startwith' adds considerable overhead. --- qubes/device_protocol.py | 11 ++++++++--- qubes/ext/pci.py | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 0b5f4b56a..232848ea2 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -858,18 +858,23 @@ def _load_classes(cls, bus: str): subclass_id = None for line in pciids.readlines(): line = line.rstrip() + if not line: + continue + first_char = line[0] if ( - line.startswith("\t\t") + first_char == "\t" + and len(line) >= 2 + and line[1] == "\t" and class_id is not None and subclass_id is not None ): progif_id, _, progif_name = line[2:].split(" ", 2) result[class_id + subclass_id + progif_id] = progif_name - elif line.startswith("\t") and class_id: + elif first_char == "\t" and class_id: subclass_id, _, subclass_name = line[1:].split(" ", 2) # store both prog-if specific entry and generic one result[class_id + subclass_id + "**"] = subclass_name - elif line.startswith("C "): + elif first_char == "C" and len(line) >= 2 and line[1] == " ": _, class_id, _, class_name = line.split(" ", 3) result[class_id + "****"] = class_name subclass_id = None diff --git a/qubes/ext/pci.py b/qubes/ext/pci.py index bc06d44f9..3d38c9c25 100644 --- a/qubes/ext/pci.py +++ b/qubes/ext/pci.py @@ -64,15 +64,24 @@ def load_pci_classes(): subclass_id = None for line in pciids.readlines(): line = line.rstrip() - if line.startswith("\t\t") and class_id and subclass_id: + if not line: + continue + first_char = line[0] + if ( + first_char == "\t" + and len(line) >= 2 + and line[1] == "\t" + and class_id is not None + and subclass_id is not None + ): progif_id, _, class_name = line[2:].split(" ", 2) result[class_id + subclass_id + progif_id] = class_name - elif line.startswith("\t") and class_id: + elif first_char == "\t" and class_id: subclass_id, _, class_name = line[1:].split(" ", 2) # store both prog-if specific entry and generic one result[class_id + subclass_id + "00"] = class_name result[class_id + subclass_id] = class_name - elif line.startswith("C "): + elif first_char == "C" and len(line) >= 2 and line[1] == " ": _, class_id, _, class_name = line.split(" ", 3) result[class_id + "0000"] = class_name result[class_id + "00"] = class_name From 193adc0ddc831edbf0ed420d2d800976659a1d7a Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 4 Jun 2026 11:09:59 +0200 Subject: [PATCH 4/7] Query only PCI devices from libvirt API Libvirt API can be quite slow on the methods for "listAllDevices" and "XMLDesc", aggravated in loops. --- qubes/ext/pci.py | 9 ++------- qubes/tests/devices_pci.py | 26 ++++++-------------------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/qubes/ext/pci.py b/qubes/ext/pci.py index 3d38c9c25..de341f129 100644 --- a/qubes/ext/pci.py +++ b/qubes/ext/pci.py @@ -364,16 +364,11 @@ def __init__(self): @qubes.ext.handler("device-list:pci") def on_device_list_pci(self, vm, event): # pylint: disable=unused-argument - # only dom0 expose PCI devices + # only dom0 exposes PCI devices if vm.qid != 0: return - for dev in vm.app.vmm.libvirt_conn.listAllDevices(): - if "pci" not in dev.listCaps(): - continue - - xml_desc = lxml.etree.fromstring(dev.XMLDesc()) - libvirt_name = xml_desc.findtext("name") + for libvirt_name in vm.app.vmm.libvirt_conn.listDevices("pci"): try: yield PCIDevice( Port(backend_domain=vm, port_id=None, devclass="pci"), diff --git a/qubes/tests/devices_pci.py b/qubes/tests/devices_pci.py index 5fe480355..8fe36d7d9 100644 --- a/qubes/tests/devices_pci.py +++ b/qubes/tests/devices_pci.py @@ -52,8 +52,8 @@ def __hash__(self): PCI_XML = """ - pci_{}_00_14_0 - /sys/devices/pci{}:00/{}:00:14.0 + pci_{address}_00_14_0 + /sys/devices/pci{address}:00/{address}:00:14.0 computer pciback @@ -196,25 +196,11 @@ def test_000_unsupported_device(self): **{ "vmm.offline_mode": False, "vmm.libvirt_conn.nodeDeviceLookupByName.return_value": mock.Mock( - **{"XMLDesc.return_value": PCI_XML.format(*["0000"] * 3)} + **{"XMLDesc.return_value": PCI_XML.format(address="0000")} ), - "vmm.libvirt_conn.listAllDevices.return_value": [ - mock.Mock( - **{ - "XMLDesc.return_value": PCI_XML.format( - *["0000"] * 3 - ), - "listCaps.return_value": ["pci"], - } - ), - mock.Mock( - **{ - "XMLDesc.return_value": PCI_XML.format( - *["10000"] * 3 - ), - "listCaps.return_value": ["pci"], - } - ), + "vmm.libvirt_conn.listDevices.return_value": [ + "pci_0000_00_14_0", + "pci_10000_00_14_0", ], } ) From 9f61d222c3a2d0ade26ab8bd90b231077b741f9f Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Sun, 7 Jun 2026 22:34:24 +0200 Subject: [PATCH 5/7] Avoid looping host devices that aren't PCI --- qubes/ext/pci.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/qubes/ext/pci.py b/qubes/ext/pci.py index de341f129..c1ac10e73 100644 --- a/qubes/ext/pci.py +++ b/qubes/ext/pci.py @@ -392,14 +392,11 @@ def on_device_list_attached(self, vm, event, **kwargs): return xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc()) - for hostdev in xml_desc.findall("devices/hostdev"): - if hostdev.get("type") != "pci": - continue - address = hostdev.find("source/address") - segment = address.get("domain")[2:] - bus = address.get("bus")[2:] - device = address.get("slot")[2:] - function = address.get("function")[2:] + for hostdev in xml_desc.findall("devices/hostdev[@type='pci']/source/address"): + segment = hostdev.get("domain")[2:] + bus = hostdev.get("bus")[2:] + device = hostdev.get("slot")[2:] + function = hostdev.get("function")[2:] libvirt_name = "pci_{segment}_{bus}_{device}_{function}".format( segment=segment, From 7c524c326c4326a316b1d733ddf713ed0bb11da8 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Tue, 9 Jun 2026 21:29:05 +0200 Subject: [PATCH 6/7] Avoid code duplication --- qubes/ext/block.py | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 3181ee281..17ac8b33b 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -435,28 +435,19 @@ def on_device_list_attached(self, vm, event, **kwargs): xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc()) for disk in xml_desc.findall("devices/disk"): - if disk.get("type") != "block": - continue - dev_path_node = disk.find("source") - if dev_path_node is None: + info = _try_get_block_device_info(vm.app, disk) + if info is None: continue - dev_path = dev_path_node.get("dev") + backend_domain, port_id = info target_node = disk.find("target") - if target_node is not None: - frontend_dev = target_node.get("dev") - if not frontend_dev: - continue - if frontend_dev in system_disks: - continue - else: + if target_node is None: + continue + frontend_dev = target_node.get("dev") + if not frontend_dev: + continue + if frontend_dev in system_disks: continue - - backend_domain_node = disk.find("backenddomain") - if backend_domain_node is not None: - backend_domain = vm.app.domains[backend_domain_node.get("name")] - else: - backend_domain = vm.app.domains[0] options = {} read_only_node = disk.find("readonly") @@ -468,13 +459,6 @@ def on_device_list_attached(self, vm, event, **kwargs): if disk.get("device") != "disk": options["devtype"] = disk.get("device") - if dev_path.startswith("/dev/"): - port_id = dev_path[len("/dev/") :] - else: - port_id = dev_path - - port_id = port_id.replace("/", "_") - yield BlockDevice(Port(backend_domain, port_id, "block")), options @staticmethod From fe99eab0dda273366840aae9a20ae9632075ef08 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Tue, 9 Jun 2026 23:04:23 +0200 Subject: [PATCH 7/7] Fine-grained XML query for cleanliness Although it doesn't help reduce the time to get the XML, as XMLDesc() is super slow, this helps cleanup the code a bit. --- qubes/ext/block.py | 40 ++++++++++++++------------------------- qubes/ext/pci.py | 4 +++- qubes/storage/__init__.py | 31 +++++++++++++++++------------- qubes/vm/qubesvm.py | 17 ++++++++--------- 4 files changed, 43 insertions(+), 49 deletions(-) diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 17ac8b33b..0b185f295 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -230,11 +230,8 @@ def attachment(self) -> Optional[QubesVM]: def _is_attached_to(self, vm): xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc()) - for disk in xml_desc.findall("devices/disk"): - info = _try_get_block_device_info(vm.app, disk) - if not info: - continue - backend_domain, port_id = info + for disk in xml_desc.findall("devices/disk[source][@type='block']"): + backend_domain, port_id = _try_get_block_device_info(vm.app, disk) if backend_domain.name != self.backend_domain.name: continue @@ -288,18 +285,13 @@ def _sanitize( def _try_get_block_device_info(app, disk): - if disk.get("type") != "block": - return None - dev_path_node = disk.find("source") - if dev_path_node is None: - return None - backend_domain_node = disk.find("backenddomain") if backend_domain_node is not None: backend_domain = app.domains[backend_domain_node.get("name")] else: backend_domain = app.domains[0] + dev_path_node = disk.find("source") dev_path = dev_path_node.get("dev") if dev_path.startswith("/dev/"): @@ -357,11 +349,10 @@ def get_device_attachments(vm_): xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc()) - for disk in xml_desc.findall("devices/disk"): - info = _try_get_block_device_info(vm.app, disk) - if not info: - continue - backend_domain, port_id = info + for disk in xml_desc.findall("devices/disk[source][@type='block']"): + backend_domain, port_id = _try_get_block_device_info( + vm.app, disk + ) if backend_domain != vm_: continue @@ -434,15 +425,11 @@ def on_device_list_attached(self, vm, event, **kwargs): system_disks = SYSTEM_DISKS_DOM0_KERNEL xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc()) - for disk in xml_desc.findall("devices/disk"): - info = _try_get_block_device_info(vm.app, disk) - if info is None: - continue - backend_domain, port_id = info - + for disk in xml_desc.findall( + "devices/disk[source][target][@type='block']" + ): + backend_domain, port_id = _try_get_block_device_info(vm.app, disk) target_node = disk.find("target") - if target_node is None: - continue frontend_dev = target_node.get("dev") if not frontend_dev: continue @@ -456,8 +443,9 @@ def on_device_list_attached(self, vm, event, **kwargs): else: options["read-only"] = "no" options["frontend-dev"] = frontend_dev - if disk.get("device") != "disk": - options["devtype"] = disk.get("device") + devtype = disk.get("device") + if devtype != "disk": + options["devtype"] = devtype yield BlockDevice(Port(backend_domain, port_id, "block")), options diff --git a/qubes/ext/pci.py b/qubes/ext/pci.py index c1ac10e73..5a6c03b2b 100644 --- a/qubes/ext/pci.py +++ b/qubes/ext/pci.py @@ -392,7 +392,9 @@ def on_device_list_attached(self, vm, event, **kwargs): return xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc()) - for hostdev in xml_desc.findall("devices/hostdev[@type='pci']/source/address"): + for hostdev in xml_desc.findall( + "devices/hostdev[@type='pci']/source/address" + ): segment = hostdev.get("domain")[2:] bus = hostdev.get("bus")[2:] device = hostdev.get("slot")[2:] diff --git a/qubes/storage/__init__.py b/qubes/storage/__init__.py index 3bbe41701..7fedcf686 100644 --- a/qubes/storage/__init__.py +++ b/qubes/storage/__init__.py @@ -657,24 +657,28 @@ def attach(self, volume, rw=False): def _is_already_attached(self, volume): """Checks if the given volume is already attached""" + # TODO: ben: cache parsed_xml = lxml.etree.fromstring(self.vm.libvirt_domain.XMLDesc()) - disk_sources = parsed_xml.xpath("//domain/devices/disk/source") - for source in disk_sources: - if source.get("dev") == "/dev/%s" % volume.vid: - return True + disks = parsed_xml.xpath( + "//domain/devices/disk/source[@dev='/dev/$vid']", vid=volume.vid + ) + if disks: + return True return False def detach(self, volume): """Detach a volume from domain""" + # TODO: ben: cache parsed_xml = lxml.etree.fromstring(self.vm.libvirt_domain.XMLDesc()) - disks = parsed_xml.xpath("//domain/devices/disk") - for disk in disks: - source = disk.xpath("source")[0] - if source.get("dev") == "/dev/%s" % volume.vid: - disk_xml = lxml.etree.tostring(disk, encoding="utf-8") - self.vm.libvirt_domain.detachDevice(disk_xml) - return - raise StoragePoolException("Volume {!r} is not attached".format(volume)) + disks = parsed_xml.xpath( + "//domain/devices/disk/source[@dev='/dev/$vid']", vid=volume.vid + ) + if not disks: + raise StoragePoolException( + "Volume {!r} is not attached".format(volume) + ) + disk_xml = lxml.etree.tostring(disks[0], encoding="utf-8") + self.vm.libvirt_domain.detachDevice(disk_xml) @property def kernels_dir(self): @@ -878,11 +882,12 @@ def unused_frontend(self): @property def used_frontends(self): """Used device names""" + # TODO: ben: cache xml = self.vm.libvirt_domain.XMLDesc() parsed_xml = lxml.etree.fromstring(xml) return { target.get("dev", None) - for target in parsed_xml.xpath("//domain/devices/disk/target") + for target in parsed_xml.xpath("//domain/devices/disk/target[@dev]") } async def export(self, volume): diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index b1f1b3007..3f7208b0b 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1030,15 +1030,14 @@ def attached_volumes(self): result = [] xml_desc = self.libvirt_domain.XMLDesc() xml = lxml.etree.fromstring(xml_desc) - for disk in xml.xpath("//domain/devices/disk"): - if disk.find("backenddomain") is not None: - pool_name = "p_%s" % disk.find("backenddomain").get("name") - pool = self.app.pools[pool_name] - vid = disk.find("source").get("dev").split("/dev/")[1] - for volume in pool.volumes: - if volume.vid == vid: - result += [volume] - break + for disk in xml.xpath("//domain/devices/disk[source][backenddomain]"): + pool_name = "p_%s" % disk.find("backenddomain").get("name") + pool = self.app.pools[pool_name] + vid = disk.find("source").get("dev").split("/dev/")[1] + for volume in pool.volumes: + if volume.vid == vid: + result += [volume] + break return result + list(self.volumes.values())