1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

perf(instances): the Stats tab reads a VM's counters in one libvirt call

Each poll called info(), then blockStats() for every disk and
interfaceStats() for every NIC: nine calls for a VM with four disks and
two NICs, every 5 s for each open Stats tab. domainListGetStats now gives
the CPU time and every disk's and NIC's counters at once; a counter the
host does not report is -1, as before.
This commit is contained in:
catborise 2026-11-05 18:40:52 +03:00
parent 34dd61c80c
commit d106a4f7ba
2 changed files with 34 additions and 15 deletions

View file

@ -15,6 +15,9 @@ try:
from libvirt import (
VIR_DOMAIN_AFFECT_CONFIG,
VIR_DOMAIN_AFFECT_LIVE,
VIR_DOMAIN_STATS_BLOCK,
VIR_DOMAIN_STATS_CPU_TOTAL,
VIR_DOMAIN_STATS_INTERFACE,
VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP,
VIR_DOMAIN_PAUSED,
VIR_DOMAIN_RUNNING,
@ -92,6 +95,7 @@ HOST_DEVICES = "devices/hostdev | devices/interface[@type='hostdev'] | devices/i
SNAPSHOT_NAME = re.compile(r"[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}")
# the longest window wvmInstance.usage() averages over
USAGE_MAX_WINDOW = 30
USAGE_STATS = VIR_DOMAIN_STATS_CPU_TOTAL | VIR_DOMAIN_STATS_BLOCK | VIR_DOMAIN_STATS_INTERFACE
# seconds the guest agent has to set a password or add a key
AGENT_TIMEOUT = 10
# A running VM is cloned with a push backup that keeps the VM alive when the
@ -1434,15 +1438,24 @@ class wvmInstance(wvmConnect):
}
key = [self.instance.ID(), self.host_info[2], disks, nics]
unread = (-1,) * 8
unread = [-1, -1]
def read():
blocks = [self.instance.blockStats(dev) for dev in disks]
links = [self.instance.interfaceStats(target) if target else unread for _, target in nics]
# one call for the CPU time and every disk's and NIC's counters
[(_dom, stats)] = self.wvm.domainListGetStats([self.instance], USAGE_STATS)
def counters(kind, names):
"""{device name: [its counters]} of stats' kind (block, net); -1 for one not reported."""
return {stats.get(f"{kind}.{i}.name"): [stats.get(f"{kind}.{i}.{name}", -1) for name in names]
for i in range(stats.get(f"{kind}.count", 0))}
blocks = counters("block", ("rd.bytes", "wr.bytes"))
links = counters("net", ("rx.bytes", "tx.bytes"))
return [
self.instance.info()[4],
[[stats[1], stats[3]] for stats in blocks],
[[stats[0], stats[4]] for stats in links],
# a driver without these stats (libvirt's test driver) still has info()
stats["cpu.time"] if "cpu.time" in stats else self.instance.info()[4],
[blocks.get(dev, unread) for dev in disks],
[links.get(target, unread) if target else unread for _, target in nics],
time.monotonic(),
]

View file

@ -4,6 +4,7 @@ devices; otherwise over one second. The stats view runs in a sync worker,
polled every 5 s by each open VM page."""
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from django.conf import settings
@ -40,20 +41,24 @@ class FakeDomain:
def info(self):
return [self.state, 0, 0, 2, int(self.now() * 500_000_000)] # 0.5 s of CPU time per second
def blockStats(self, dev):
n = {"vda": 1, "vdb": 2}[dev] # disks are read by target name
written = -1 if self.unsupported and dev == "vdb" else int(self.now() * n * 3000)
return (0, int(self.now() * n * 1000), 0, written, 0)
def interfaceStats(self, dev):
assert dev == "vnet0"
return (int(self.now() * 100), 0, 0, 0, int(self.now() * 50), 0, 0, 0)
def stats(self):
"""domainListGetStats' counters: disks and NICs by name, as libvirt
lists them (the CD-ROM too); a counter the host does not report is left out."""
stats = {"cpu.time": int(self.now() * 500_000_000), "block.count": 3, "net.count": 1} # 0.5 s of CPU a second
for i, (name, n) in enumerate((("vda", 1), ("vdb", 2), ("sda", 0))):
stats[f"block.{i}.name"] = name
stats[f"block.{i}.rd.bytes"] = int(self.now() * n * 1000)
if not (self.unsupported and name == "vdb"):
stats[f"block.{i}.wr.bytes"] = int(self.now() * n * 3000)
stats.update({"net.0.name": "vnet0", "net.0.rx.bytes": int(self.now() * 100), "net.0.tx.bytes": int(self.now() * 50)})
return stats
def instance(state, clock):
proxy = wvmInstance.__new__(wvmInstance) # no connection: only these calls
proxy._read_cache = None
proxy.instance = FakeDomain(state, clock)
proxy.wvm = SimpleNamespace(domainListGetStats=lambda doms, kinds: [(doms[0], doms[0].stats())])
proxy._XMLDesc = lambda flags: XML
proxy.host_info = ["x86_64", 0, 4] # 4 host CPUs
proxy.get_status = lambda: state
@ -151,7 +156,8 @@ class UsageTestCase(unittest.TestCase):
def restart(seconds):
real_sleep(seconds)
domain.info = lambda: [1, 0, 0, 2, 0] # CPU time starts over
counters = domain.stats
domain.stats = lambda: {**counters(), "cpu.time": 0} # CPU time starts over
self.time.sleep.side_effect = restart
result = proxy.usage()