From bf736942eb1524bd1be14eb34854d9a389cd7912 Mon Sep 17 00:00:00 2001 From: catborise Date: Sat, 7 Nov 2026 15:25:49 +0300 Subject: [PATCH] perf(computes): a host's capacity is one libvirt call, not one per VM The Computes page, the host overview and the new-VM dialog's capacity line read each VM's XML for its memory and vCPUs: a round trip a VM, about 200 ms for 150 VMs on a LAN, more with each host. libvirt's domain stats give them for every VM in one call; a running VM busy with a job is not waited for (NOWAIT) and its sizes come from its XML as before. --- vrtManager/hostdetails.py | 24 ++++++++++++++++++++++-- vrtManager/tests/test_host_reads.py | 25 +++++++++++++++---------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/vrtManager/hostdetails.py b/vrtManager/hostdetails.py index 1ff700ea..0072cb60 100644 --- a/vrtManager/hostdetails.py +++ b/vrtManager/hostdetails.py @@ -2,12 +2,17 @@ import time from libvirt import ( libvirtError, + VIR_CONNECT_GET_ALL_DOMAINS_STATS_NOWAIT, VIR_DOMAIN_BLOCKED, VIR_DOMAIN_PAUSED, VIR_DOMAIN_RUNNING, VIR_DOMAIN_SHUTOFF, + VIR_DOMAIN_STATS_BALLOON, + VIR_DOMAIN_STATS_STATE, + VIR_DOMAIN_STATS_VCPU, VIR_NODE_CPU_STATS_ALL_CPUS, ) +from lxml import etree from vrtManager.connection import wvmConnect from vrtManager.util import get_xml_path @@ -111,8 +116,7 @@ class wvmHostDetails(wvmConnect): vcpus = {"all": 0, "running": 0} assigned = {"all": 0, "running": 0} vms = {"running": 0, "paused": 0, "shutoff": 0, "other": 0} - # the VM lists' read: the states in one call, each VM's XML once - for (state, _max_memory, memory, vcpu), _title in self.get_domain_rows().values(): + for state, memory, vcpu in self._domain_sizes(): for share in ("all",) if state == VIR_DOMAIN_SHUTOFF else ("all", "running"): vcpus[share] += vcpu assigned[share] += memory * 1024 @@ -120,6 +124,22 @@ class wvmHostDetails(wvmConnect): return {"cpus": self.host_info[2], "memory": self.host_info[1] * 1048576, "vcpus": vcpus, "assigned": assigned, "vms": vms} + def _domain_sizes(self): + """Each VM's state, memory (KiB) and vCPUs, in one call: not its XML, + one round trip a VM. A running VM busy with a job (a migration, a + hung QEMU) is not waited for: its sizes come from its XML.""" + flags = VIR_DOMAIN_STATS_STATE | VIR_DOMAIN_STATS_BALLOON | VIR_DOMAIN_STATS_VCPU + for dom, stats in self.wvm.getAllDomainStats(flags, VIR_CONNECT_GET_ALL_DOMAINS_STATS_NOWAIT): + memory, vcpu = stats.get("balloon.current"), stats.get("vcpu.current") + if memory is None or vcpu is None: + try: + doc = etree.fromstring(dom.XMLDesc(0)) + except libvirtError: + continue # gone meanwhile + memory, vcpu = int(doc.findtext("currentMemory")), doc.find("vcpu") + vcpu = int(vcpu.get("current", vcpu.text)) + yield stats["state.state"], memory, vcpu + def get_cpu_topology(self): """ The host's logical CPUs and MHz, and from the CPUs of its NUMA cells diff --git a/vrtManager/tests/test_host_reads.py b/vrtManager/tests/test_host_reads.py index 30e741a1..70f41d66 100644 --- a/vrtManager/tests/test_host_reads.py +++ b/vrtManager/tests/test_host_reads.py @@ -158,19 +158,24 @@ class CapacityTestCase(unittest.TestCase): conn = connection(wvmHostDetails) gib = 1024 ** 3 kib = gib // 1024 - # get_domain_rows: info (state, max memory KiB, memory KiB, vCPUs) and title - conn.get_domain_rows = MagicMock(return_value={ - "u-web": ([VIR_DOMAIN_RUNNING, 8 * kib, 4 * kib, 4], ""), - "u-db": ([VIR_DOMAIN_PAUSED, 2 * kib, 2 * kib, 2], ""), - "u-old": ([VIR_DOMAIN_SHUTOFF, 8 * kib, 8 * kib, 8], ""), - }) + busy = MagicMock(**{"XMLDesc.return_value": f"{kib}" + "4"}) + idle = MagicMock() + # one call for every VM; a VM busy with a job has only its state: its XML gives the rest + conn.wvm.getAllDomainStats = MagicMock(return_value=[ + (idle, {"state.state": VIR_DOMAIN_RUNNING, "balloon.current": 4 * kib, "vcpu.current": 4}), + (idle, {"state.state": VIR_DOMAIN_PAUSED, "balloon.current": 2 * kib, "vcpu.current": 2}), + (idle, {"state.state": VIR_DOMAIN_SHUTOFF, "balloon.current": 8 * kib, "vcpu.current": 8}), + (busy, {"state.state": VIR_DOMAIN_RUNNING}), + ]) self.assertEqual(conn.get_capacity(), { "cpus": 4, "memory": 2 * gib, - "vcpus": {"all": 14, "running": 6}, - "assigned": {"all": 14 * gib, "running": 6 * gib}, - "vms": {"running": 1, "paused": 1, "shutoff": 1, "other": 0}, + "vcpus": {"all": 15, "running": 7}, + "assigned": {"all": 15 * gib, "running": 7 * gib}, + "vms": {"running": 2, "paused": 1, "shutoff": 1, "other": 0}, }) - conn.get_domain_rows.assert_called_once_with() + self.assertEqual(conn.wvm.getAllDomainStats.call_count, 1) + idle.XMLDesc.assert_not_called() class CpuTopologyTestCase(unittest.TestCase):