From 0658eef5bea342725e0c844f8d1b292e8b82ed17 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 11 Dec 2025 20:52:53 +0100 Subject: [PATCH 1/8] Ignore global preload setting on disp performance For: https://github.com/QubesOS/qubes-issues/issues/1512 --- qubes/tests/integ/dispvm_perf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qubes/tests/integ/dispvm_perf.py b/qubes/tests/integ/dispvm_perf.py index 960bc8b25..5f72a0635 100644 --- a/qubes/tests/integ/dispvm_perf.py +++ b/qubes/tests/integ/dispvm_perf.py @@ -41,6 +41,8 @@ def setUp(self: qubes.tests.SystemTestCase): self.test_dir = tempfile.mkdtemp(prefix=prefix) self.vm2 = None return + self.app.domains["dom0"].features["preload-dispvm-max"] = "" + self.app.domains["dom0"].features["preload-dispvm-threshold"] = "" self.dvm = self.app.add_new_vm( "AppVM", name=self.make_vm_name("dvm"), From 780c16e24c37a60755a98ad23d8c37e047cf28bf Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 23 Oct 2025 11:40:58 +0200 Subject: [PATCH 2/8] Add drive information to HCL Disk information is already being reported for SCSI, but modern laptops may not have ATA and only have PCIe for NVMe. Adding this information to disposable performance report may help understand the execution speed. For: https://github.com/QubesOS/qubes-issues/issues/1512 --- qvm-tools/qubes-hcl-report | 12 ++++++++++++ tests/dispvm_perf.py | 2 ++ tests/dispvm_perf_reader.py | 8 +++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/qvm-tools/qubes-hcl-report b/qvm-tools/qubes-hcl-report index d71b76a38..e8540697a 100755 --- a/qvm-tools/qubes-hcl-report +++ b/qvm-tools/qubes-hcl-report @@ -103,6 +103,11 @@ cat /etc/qubes-release > "$TEMP_DIR/qubes-release" || exit cat /proc/cpuinfo > "$TEMP_DIR/cpuinfo" || exit sudo lspci -nnvk > "$TEMP_DIR/lspci" cat /proc/scsi/scsi > "$TEMP_DIR/scsi" +for drive in /sys/class/nvme-subsystem/nvme-subsys*/; do + test -d "$drive" || continue + cat "$drive"/{serial,model,firmware_rev} | + tr "\n" " " | sed "s/\s$//" > "$TEMP_DIR/nvme" +done sudo dmidecode > "$TEMP_DIR/dmidecode" xl info > "$TEMP_DIR/xl-info" || exit xl dmesg > "$TEMP_DIR/xl-dmesg" || exit @@ -204,6 +209,11 @@ CHIPSET=$(grep "00:00.0.*Host bridge" "$TEMP_DIR/lspci" |cut -d ':' -f3- |sed -e VGA=$(grep -E 'VGA|Display' "$TEMP_DIR/lspci" |cut -d ':' -f3- |sed -e "s/^[[:space:]]*/\ /") NET=$(grep -E 'Network|Ethernet' "$TEMP_DIR/lspci" |cut -d ':' -f3- |sed -e "s/^[[:space:]]*/\ /") SCSI=$(grep Model "$TEMP_DIR/scsi"|cut -d ':' -f3-|sed -e "s/^[[:space:]]*/\ /") +if test -s "$TEMP_DIR/nvme"; then + NVME=$(sed -e "s/^[[:space:]]*/ /" "$TEMP_DIR/nvme") +else + NVME="" +fi RAM=$(grep total_memory "$TEMP_DIR/xl-info"|cut -d ':' -f2 |tr -d ' ') USB=$(grep -c USB "$TEMP_DIR/lspci") XEN_MAJOR=$(grep xen_major "$TEMP_DIR/xl-info"|cut -d: -f2 |tr -d ' ') @@ -378,6 +388,8 @@ memory: | $RAM scsi: | $SCSI +nvme: | +$NVME usb: | $USB certified: diff --git a/tests/dispvm_perf.py b/tests/dispvm_perf.py index 5b5d09c44..42eb5a6f9 100755 --- a/tests/dispvm_perf.py +++ b/tests/dispvm_perf.py @@ -398,6 +398,8 @@ def hcl() -> dict: "hcl-model": report["model"].rstrip(), "hcl-bios": report["bios"].rstrip(), "hcl-cpu": report["cpu"].rstrip(), + "hcl-scsi": report["scsi"].rstrip(), + "hcl-nvme": report["nvme"].rstrip(), } ) return data diff --git a/tests/dispvm_perf_reader.py b/tests/dispvm_perf_reader.py index cab868139..be7843ec1 100755 --- a/tests/dispvm_perf_reader.py +++ b/tests/dispvm_perf_reader.py @@ -566,6 +566,8 @@ def graph_00_specs(self) -> None: "hcl-model": data["hcl-model"], "hcl-bios": data["hcl-bios"], "hcl-cpu": data["hcl-cpu"], + "hcl-scsi": data.get("hcl-scsi", None), + "hcl-nvme": data.get("hcl-nvme", None), } specs_text = """ System specifications: @@ -580,6 +582,8 @@ def graph_00_specs(self) -> None: - RAM: {} MiB - CPU: {} - BIOS: {} + - SCSI: {} + - NVMe: {} """.format( specs["date"], self.default_template, @@ -591,6 +595,8 @@ def graph_00_specs(self) -> None: specs["hcl-memory"], specs["hcl-cpu"], specs["hcl-bios"], + specs["hcl-scsi"], + specs["hcl-nvme"], ) fig = plt.figure(figsize=(2 * WIDTH, 2 * HEIGHT)) fig.clf() @@ -950,7 +956,7 @@ def graph_09_method(self) -> None: preload_max = get_value(preload_tests, "preload_max")[0] caption = ( f"Compares workflows of normal disposables with " - + "{preload_max} preloaded disposables." + + f"{preload_max} preloaded disposables." ) self.bar_plot( [dom0_api, dom0_qvm, vm_qrexec], From 0ee6f3cc6997a14b1c05d08ba73855a1b49a5a2e Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 6 Nov 2025 11:05:18 +0100 Subject: [PATCH 3/8] Add detailed template and hardware data to spec Also add a common prefix so files for different templates are sorted together on OpenQA. For: https://github.com/QubesOS/qubes-issues/issues/1512 --- tests/dispvm_perf_reader.py | 83 ++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/tests/dispvm_perf_reader.py b/tests/dispvm_perf_reader.py index be7843ec1..2c1d12674 100755 --- a/tests/dispvm_perf_reader.py +++ b/tests/dispvm_perf_reader.py @@ -372,7 +372,7 @@ def end(self, plot, name: str = "", skip_fname: bool = False) -> None: else: if not skip_fname: name = get_fname() - name = self.default_template + "_" + name + name = "dispvm_perf-" + self.default_template + "_" + name if self.output_dir: logging.info("Saving figure %s", name) @@ -555,48 +555,53 @@ def graph_00_specs(self) -> None: """System specifications graph.""" first_test = list(self.data.keys())[0] data = self.data[first_test] - specs = { - "date": data["date"], - "template-buildtime": data["template-buildtime"], - "kernel": data["kernel"], - "hcl-memory": data["hcl-memory"], - "hcl-certified": data["hcl-certified"], - "hcl-qubes": data["hcl-qubes"], - "hcl-xen": data["hcl-xen"], - "hcl-model": data["hcl-model"], - "hcl-bios": data["hcl-bios"], - "hcl-cpu": data["hcl-cpu"], - "hcl-scsi": data.get("hcl-scsi", None), - "hcl-nvme": data.get("hcl-nvme", None), - } specs_text = """ System specifications: - - Date: {} - - Template: {} - - Template build time: {} - - Certified: {} - - Qubes: {} - - Kernel: {} - - Xen: {} - - RAM: {} MiB - - CPU: {} - - BIOS: {} - - SCSI: {} - - NVMe: {} + - Global: + - Date: {date} + - Qubes: {hcl_qubes} + - Xen: {hcl_xen} + - Global Kernel: {hcl_kernel} + - Template: + - Name: {template} + - Build time: {template_buildtime} + - Last update: {template_last_update} + - Virtual CPUs: {vcpus} + - Bootstrap memory: {memory} + - Maximum memory: {maxmem} + - Kernel: {kernel} + - Kernel options: {kernelopts} + - Hardware: + - Certified: {hcl_certified} + - Brand: {hcl_brand} + - Model: {hcl_model} + - CPU: {hcl_cpu} + - RAM: {hcl_memory} MiB + - BIOS: {hcl_bios} + - SCSI: {hcl_scsi} + - NVMe: {hcl_nvme} """.format( - specs["date"], - self.default_template, - specs["template-buildtime"], - specs["hcl-certified"], - specs["hcl-qubes"], - specs["kernel"], - specs["hcl-xen"], - specs["hcl-memory"], - specs["hcl-cpu"], - specs["hcl-bios"], - specs["hcl-scsi"], - specs["hcl-nvme"], + date=data["date"], + template=self.default_template, + template_buildtime=data["template-buildtime"], + template_last_update=data["last-update"] or None, + memory=data["memory"], + maxmem=data["maxmem"], + vcpus=data["vcpus"], + kernel=data["kernel"], + kernelopts=data["kernelopts"], + hcl_memory=data["hcl-memory"], + hcl_certified=data.get("hcl-certified"), + hcl_qubes=data["hcl-qubes"], + hcl_xen=data["hcl-xen"], + hcl_kernel=data["hcl-kernel"], + hcl_brand=data.get("hcl-brand"), + hcl_model=data.get("hcl-model"), + hcl_bios=data.get("hcl-bios"), + hcl_cpu=data.get("hcl-cpu"), + hcl_scsi=data.get("hcl-scsi"), + hcl_nvme=data.get("hcl-nvme"), ) fig = plt.figure(figsize=(2 * WIDTH, 2 * HEIGHT)) fig.clf() From 24c29f46e34cfe8db39dafa2d911543c9fa990c1 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Wed, 26 Nov 2025 21:07:56 +0100 Subject: [PATCH 4/8] Lint test files Avoid syntactically wrong tests from reaching OpenQA. --- .pylintrc | 8 +- qubes/ext/utils.py | 2 +- qubes/qmemman/systemstate.py | 2 +- qubes/rngdoc.py | 2 +- qubes/tests/api.py | 9 +- qubes/tests/api_internal.py | 4 +- qubes/tests/api_misc.py | 10 +-- qubes/tests/app.py | 131 ++++++++++++++---------------- qubes/tests/ext.py | 70 ++++++++-------- qubes/tests/integ/dispvm.py | 113 ++++++++++++-------------- qubes/tests/integ/network.py | 29 +++---- qubes/tests/integ/network_ipv6.py | 30 +++---- qubes/tests/vm/adminvm.py | 10 +-- qubes/tests/vm/appvm.py | 17 ++-- qubes/tests/vm/dispvm.py | 58 ++++++------- qubes/tests/vm/mix/dvmtemplate.py | 6 +- qubes/tests/vm/mix/net.py | 3 +- qubes/tests/vm/qubesvm.py | 3 - qubes/vm/appvm.py | 1 + 19 files changed, 237 insertions(+), 271 deletions(-) diff --git a/.pylintrc b/.pylintrc index eaa3a43a1..9b5ca6970 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,6 +1,6 @@ [MASTER] persistent=no -ignore-paths=qubes/tests +ignore-paths=^qubes/tests/(?!integ/(dispvm|network).*$)(?!(app|api|ext|vm/(adminvm|appvm|dispvm|mix/(dvmtemplate|net))).py$).* init-hook="import astroid.bases; astroid.bases.POSSIBLE_PROPERTIES.add('stateless_property')" [MESSAGES CONTROL] @@ -61,10 +61,10 @@ class-rgx=([A-Z_][a-zA-Z0-9]+|TC_\d\d_[a-zA-Z0-9_]+)$ function-rgx=(test_[0-9]{3}_[a-z0-9_]{2,50}|[a-z_][a-z0-9_]{2,50})$ # Regular expression which should only match correct method names -method-rgx=(test_[0-9]{3}_[a-z0-9_]{2,50}|[a-z_][a-z0-9_]{2,50})$ +method-rgx=(setUp|setUpClass|tearDown|tearDownClass|test_[0-9]{3}_[a-z0-9_]{2,50}|[a-z_][a-z0-9_]{2,50})$ # Regular expression which should only match correct instance attribute names -attr-rgx=[a-z_][a-z0-9_]{2,40}$ +attr-rgx=(maxDiff|[a-z_][a-z0-9_]{2,40})$ # Regular expression which should only match correct argument names argument-rgx=[a-z_][a-z0-9_]{2,40}$ @@ -198,6 +198,6 @@ max-positional-arguments=7 # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception,EnvironmentError +overgeneral-exceptions=builtins.Exception,builtins.EnvironmentError # vim: ft=conf diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index c0c89d993..832d2ba7d 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -169,7 +169,7 @@ def compare_device_cache(vm, devices_cache, current_devices): async def confirm_device_attachment(device, frontends) -> str: try: return await _do_confirm_device_attachment(device, frontends) - except Exception as exc: + except Exception as exc: # pylint: disable=broad-exception-caught print(str(exc.__class__.__name__) + ":", str(exc), file=sys.stderr) return "" diff --git a/qubes/qmemman/systemstate.py b/qubes/qmemman/systemstate.py index 8a8abe3a1..baf4585ab 100644 --- a/qubes/qmemman/systemstate.py +++ b/qubes/qmemman/systemstate.py @@ -183,7 +183,7 @@ def mem_set(self, domid, val) -> None: int(domid), int(val / 1024) + 1024 ) # LIBXL_MAXMEM_CONSTANT=1024 self.xc.domain_set_target_mem(int(domid), int(val / 1024)) - except Exception: + except Exception: # pylint: disable=broad-exception-caught pass # VM sees about 16MB memory less, so adjust for it here - qmemman # handle Xen view of memory diff --git a/qubes/rngdoc.py b/qubes/rngdoc.py index 2a808b060..ae4f6622b 100755 --- a/qubes/rngdoc.py +++ b/qubes/rngdoc.py @@ -121,7 +121,7 @@ def get_child_elements(self): number = "\\+" else: print(parent.tag) - raise Exception( + raise ValueError( f"Cannot choose number format for tag {parent.tag}" ) diff --git a/qubes/tests/api.py b/qubes/tests/api.py index 80c6be18c..228fde64f 100644 --- a/qubes/tests/api.py +++ b/qubes/tests/api.py @@ -27,7 +27,7 @@ import qubes.tests -class TestMgmt(object): +class TestMgmt: def __init__(self, app, src, method, dest, arg, send_event=None): self.app = app self.src = src @@ -35,6 +35,7 @@ def __init__(self, app, src, method, dest, arg, send_event=None): self.dest = dest self.arg = arg self.send_event = send_event + self.task = None try: self.function = { "mgmt.success": self.success, @@ -67,12 +68,14 @@ async def qubesexception(self, untrusted_payload): raise qubes.exc.QubesException("qubes-exception") async def exception(self, untrusted_payload): + # pylint: disable=broad-exception-raised raise Exception("exception") async def event(self, untrusted_payload): future = asyncio.get_event_loop().create_future() class Subject: + # pylint: disable=too-few-public-methods name = "subject" def __str__(self): @@ -93,7 +96,7 @@ def __str__(self): class TC_00_QubesDaemonProtocol(qubes.tests.QubesTestCase): def setUp(self): - super(TC_00_QubesDaemonProtocol, self).setUp() + super().setUp() self.app = unittest.mock.Mock() self.app.log = self.log self.sock_client, self.sock_server = socket.socketpair() @@ -116,7 +119,7 @@ def tearDown(self): except AttributeError: # old python in travis pass self.transport.close() - super(TC_00_QubesDaemonProtocol, self).tearDown() + super().tearDown() def test_000_message_ok(self): self.writer.write(b"mgmt.success+arg src name dest\0payload") diff --git a/qubes/tests/api_internal.py b/qubes/tests/api_internal.py index 86a512f48..48cbfd844 100644 --- a/qubes/tests/api_internal.py +++ b/qubes/tests/api_internal.py @@ -26,9 +26,9 @@ import uuid -def mock_coro(f): +def mock_coro(coro): async def coro_f(*args, **kwargs): - return f(*args, **kwargs) + return coro(*args, **kwargs) return coro_f diff --git a/qubes/tests/api_misc.py b/qubes/tests/api_misc.py index cf885abcd..fafeb4ecf 100644 --- a/qubes/tests/api_misc.py +++ b/qubes/tests/api_misc.py @@ -30,7 +30,7 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase): maxDiff = None def setUp(self): - super(TC_00_API_Misc, self).setUp() + super().setUp() self.tpl = mock.NonCallableMagicMock(name="template") del self.tpl.template self.async_src = mock.AsyncMock() @@ -393,9 +393,9 @@ def test_027_notify_updates_template_based_template_running(self): self.assertEqual(self.app.mock_calls, []) def test_028_notify_updates_template_based_dispvm(self): - self.dvm = self.src - self.dvm.updateable = False - self.srv = mock.NonCallableMagicMock(template=self.dvm) + dvm = self.src + dvm.updateable = False + _srv = mock.NonCallableMagicMock(template=dvm) self.src.updateable = False self.src.features.get.return_value = False self.src.template.is_running.return_value = False @@ -424,7 +424,7 @@ def test_028_notify_updates_template_based_dispvm(self): + "\nExpected:\n" + str(expected).replace(",", ",\n"), ) - self.assertEqual(self.dvm.mock_calls, []) + self.assertEqual(dvm.mock_calls, []) self.assertIsInstance(self.src.updates_available, mock.Mock) self.assertEqual(self.app.mock_calls, [mock.call.save()]) diff --git a/qubes/tests/app.py b/qubes/tests/app.py index bbea5920b..a3ab4a4b7 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -21,8 +21,7 @@ # import os -import unittest -import unittest.mock as mock +from unittest import mock import lxml.etree @@ -139,7 +138,7 @@ class TC_20_QubesHost(qubes.tests.QubesTestCase): ] def setUp(self): - super(TC_20_QubesHost, self).setUp() + super().setUp() self.app = TestApp() self.app.vmm = mock.Mock() self.qubes_host = qubes.app.QubesHost(self.app) @@ -234,7 +233,7 @@ def test_002_get_vm_stats_one_vm(self): vm.xid = 1 vm.name = "somevm" - info_time, info = self.qubes_host.get_vm_stats(only_vm=vm) + info_time, _info = self.qubes_host.get_vm_stats(only_vm=vm) self.assertIsNotNone(info_time) self.assertEqual( self.app.vmm.mock_calls, @@ -491,7 +490,7 @@ class TC_89_QubesEmpty(qubes.tests.QubesTestCase): def tearDown(self): try: os.unlink("/tmp/qubestest.xml") - except: + except: # pylint: disable=bare-except pass try: self.app.close() @@ -558,93 +557,85 @@ def test_100_property_migrate_default_fw_netvm(self): """ with self.subTest("default_setup"): - with open("/tmp/qubestest.xml", "w") as xml_file: + with open("/tmp/qubestest.xml", "w", encoding="ascii") as xml_file: xml_file.write( xml_template.format( default_netvm="sys-firewall", default_fw_netvm="sys-net" ) ) - self.app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True) - self.assertEqual(self.app.domains["sys-net"].netvm, None) + app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True) + self.assertEqual(app.domains["sys-net"].netvm, None) self.assertEqual( - self.app.domains["sys-firewall"].netvm, - self.app.domains["sys-net"], + app.domains["sys-firewall"].netvm, + app.domains["sys-net"], ) # property is no longer "default" self.assertFalse( - self.app.domains["sys-firewall"].property_is_default("netvm") + app.domains["sys-firewall"].property_is_default("netvm") ) # verify that appvm.netvm is unaffected - self.assertTrue( - self.app.domains["appvm"].property_is_default("netvm") - ) + self.assertTrue(app.domains["appvm"].property_is_default("netvm")) self.assertEqual( - self.app.domains["appvm"].netvm, - self.app.domains["sys-firewall"], + app.domains["appvm"].netvm, + app.domains["sys-firewall"], ) with self.assertRaises(AttributeError): - self.app.default_fw_netvm + app.default_fw_netvm # pylint: disable=no-member - self.app.close() - del self.app + app.close() + del app with self.subTest("same"): - with open("/tmp/qubestest.xml", "w") as xml_file: + with open("/tmp/qubestest.xml", "w", encoding="ascii") as xml_file: xml_file.write( xml_template.format( default_netvm="sys-net", default_fw_netvm="sys-net" ) ) - self.app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True) - self.assertEqual(self.app.domains["sys-net"].netvm, None) + app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True) + self.assertEqual(app.domains["sys-net"].netvm, None) self.assertEqual( - self.app.domains["sys-firewall"].netvm, - self.app.domains["sys-net"], + app.domains["sys-firewall"].netvm, + app.domains["sys-net"], ) self.assertTrue( - self.app.domains["sys-firewall"].property_is_default("netvm") + app.domains["sys-firewall"].property_is_default("netvm") ) # verify that appvm.netvm is unaffected - self.assertTrue( - self.app.domains["appvm"].property_is_default("netvm") - ) - self.assertEqual( - self.app.domains["appvm"].netvm, self.app.domains["sys-net"] - ) + self.assertTrue(app.domains["appvm"].property_is_default("netvm")) + self.assertEqual(app.domains["appvm"].netvm, app.domains["sys-net"]) with self.assertRaises(AttributeError): - self.app.default_fw_netvm + app.default_fw_netvm # pylint: disable=no-member - self.app.close() - del self.app + app.close() + del app with self.subTest("loop"): - with open("/tmp/qubestest.xml", "w") as xml_file: + with open("/tmp/qubestest.xml", "w", encoding="ascii") as xml_file: xml_file.write( xml_template.format( default_netvm="sys-firewall", default_fw_netvm="sys-firewall", ) ) - self.app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True) - self.assertEqual(self.app.domains["sys-net"].netvm, None) + app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True) + self.assertEqual(app.domains["sys-net"].netvm, None) # this was netvm loop, better set to none, to not crash qubesd - self.assertEqual(self.app.domains["sys-firewall"].netvm, None) + self.assertEqual(app.domains["sys-firewall"].netvm, None) self.assertFalse( - self.app.domains["sys-firewall"].property_is_default("netvm") + app.domains["sys-firewall"].property_is_default("netvm") ) # verify that appvm.netvm is unaffected - self.assertTrue( - self.app.domains["appvm"].property_is_default("netvm") - ) + self.assertTrue(app.domains["appvm"].property_is_default("netvm")) self.assertEqual( - self.app.domains["appvm"].netvm, - self.app.domains["sys-firewall"], + app.domains["appvm"].netvm, + app.domains["sys-firewall"], ) with self.assertRaises(AttributeError): - self.app.default_fw_netvm + app.default_fw_netvm # pylint: disable=no-member - self.app.close() - del self.app + app.close() + del app def test_101_property_migrate_label(self): xml_template = """ @@ -672,25 +663,25 @@ def test_101_property_migrate_label(self): """ with self.subTest("replace_label"): - with open("/tmp/qubestest.xml", "w") as xml_file: + with open("/tmp/qubestest.xml", "w", encoding="ascii") as xml_file: xml_file.write(xml_template.format(old_gray="0x555753")) - self.app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True) - self.assertEqual(self.app.get_label("gray").color, "0x555555") - self.app.close() - del self.app + app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True) + self.assertEqual(app.get_label("gray").color, "0x555555") + app.close() + del app with self.subTest("dont_replace_label"): - with open("/tmp/qubestest.xml", "w") as xml_file: + with open("/tmp/qubestest.xml", "w", encoding="ascii") as xml_file: xml_file.write(xml_template.format(old_gray="0x123456")) - self.app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True) - self.assertEqual(self.app.get_label("gray").color, "0x123456") - self.app.close() - del self.app + app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True) + self.assertEqual(app.get_label("gray").color, "0x123456") + app.close() + del app class TC_90_Qubes(qubes.tests.QubesTestCase): def setUp(self): - super(TC_90_Qubes, self).setUp() + super().setUp() self.app = qubes.Qubes( "/tmp/qubestest.xml", load=False, offline_mode=True ) @@ -725,7 +716,7 @@ def setUp(self): def tearDown(self): try: os.unlink("/tmp/qubestest.xml") - except: + except: # pylint: disable=bare-except pass del self.emitter super().tearDown() @@ -813,7 +804,7 @@ class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder): def test_113_guivm(self): class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder): - guivm = qubes.property("guivm", default=(lambda self: "dom0")) + guivm = qubes.property("guivm", default=lambda self: "dom0") holder = MyTestHolder(None) guivm = self.app.add_new_vm( @@ -893,7 +884,7 @@ class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder): def test_115_audiovm(self): class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder): - audiovm = qubes.property("audiovm", default=(lambda self: "dom0")) + audiovm = qubes.property("audiovm", default=lambda self: "dom0") holder = MyTestHolder(None) audiovm = self.app.add_new_vm( @@ -968,7 +959,7 @@ def test_117_remotevm_status(self): remotevm1.get_mem(), ] == ["Running", 0, 0] - @unittest.mock.patch("qubes.vm.qubesvm.QubesVM.untrusted_qdb") + @mock.patch("qubes.vm.qubesvm.QubesVM.untrusted_qdb") def test_118_remotevm_set_relayvm(self, mock_qubesdb): class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder): relayvm = qubes.property("relayvm") @@ -1020,7 +1011,7 @@ class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder): ) def test_200_remove_template(self): - appvm = self.app.add_new_vm( + self.app.add_new_vm( "AppVM", name="test-vm", template=self.template, label="red" ) with mock.patch.object(self.app, "vmm"): @@ -1070,7 +1061,7 @@ def test_204_remove_appvm_dispvm(self): dispvm = self.app.add_new_vm( "AppVM", name="test-appvm", template=self.template, label="red" ) - appvm = self.app.add_new_vm( + self.app.add_new_vm( "AppVM", name="test-appvm2", template=self.template, @@ -1089,7 +1080,7 @@ def test_205_remove_appvm_dispvm(self): template_for_dispvms=True, label="red", ) - dispvm = self.app.add_new_vm( + self.app.add_new_vm( "DispVM", name="test-dispvm", template=appvm, label="red" ) with mock.patch.object(self.app, "vmm"): @@ -1267,9 +1258,7 @@ def test_303_preload_default_dispvm_change_partial_noop(self): @qubes.tests.skipUnlessGit def test_900_example_xml_in_doc(self): - self.assertXMLIsValid( - lxml.etree.parse( - open(os.path.join(qubes.tests.in_git, "doc/example.xml"), "rb") - ), - "qubes.rng", - ) + path = os.path.join(qubes.tests.in_git, "doc/example.xml") + with open(path, "rb") as file: + tree = lxml.etree.parse(file) + self.assertXMLIsValid(tree, "qubes.rng") diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py index 7187dc99e..e98010c91 100644 --- a/qubes/tests/ext.py +++ b/qubes/tests/ext.py @@ -19,7 +19,8 @@ # License along with this library; if not, see . import os -import unittest.mock + +from unittest import mock import qubes.ext.admin import qubes.ext.audio @@ -32,8 +33,6 @@ import qubes.tests import qubes.vm.qubesvm -from unittest import mock - class TC_00_CoreFeatures(qubes.tests.QubesTestCase): maxDiff = None @@ -233,7 +232,7 @@ def test_017_notify_tools_template_based(self): ("template.__bool__", (), {}), ( "log.warning", - ("Ignoring qubes.NotifyTools for template-based " "VM",), + ("Ignoring qubes.NotifyTools for template-based VM",), {}, ), ], @@ -430,8 +429,8 @@ def test_032_distro_meta_invalid(self): ("os-distribution-like", "debian"), {}, ), - ("log.warning", unittest.mock.ANY, {}), - ("log.warning", unittest.mock.ANY, {}), + ("log.warning", mock.ANY, {}), + ("log.warning", mock.ANY, {}), ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], @@ -462,8 +461,8 @@ def test_033_distro_meta_invalid2(self): ("os-distribution-like", "debian"), {}, ), - ("log.warning", unittest.mock.ANY, {}), - ("log.warning", unittest.mock.ANY, {}), + ("log.warning", mock.ANY, {}), + ("log.warning", mock.ANY, {}), ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], @@ -1698,19 +1697,18 @@ def setUp(self): self.vm.configure_mock( **{ "features.get.side_effect": self.features.get, - "features.check_with_template.side_effect": self.mock_check_with_template, + "features.check_with_template.side_effect": self.mock_check_tpl, "features.__contains__.side_effect": self.features.__contains__, "features.__setitem__.side_effect": self.features.__setitem__, } ) - def mock_check_with_template(self, name, default): + def mock_check_tpl(self, name, default): if hasattr(self.vm, "template"): return self.features.get( name, self.template_features.get(name, default) ) - else: - return self.features.get(name, default) + return self.features.get(name, default) def test_000_notify_tools_full(self): del self.vm.template @@ -1963,14 +1961,14 @@ def test_012_supported_services_remove(self): ) def test_013_feature_set_dom0(self): - self.test_base_dir = "/tmp/qubes-test-dir" - self.base_dir_patch = mock.patch.dict( - qubes.config.system_path, {"dom0_services_dir": self.test_base_dir} + test_base_dir = "/tmp/qubes-test-dir" + base_dir_patch = mock.patch.dict( + qubes.config.system_path, {"dom0_services_dir": test_base_dir} ) - self.base_dir_patch.start() - self.addCleanup(self.base_dir_patch.stop) + base_dir_patch.start() + self.addCleanup(base_dir_patch.stop) service = "guivm-gui-agent" - service_path = self.test_base_dir + "/" + service + service_path = test_base_dir + "/" + service self.ext.on_domain_feature_set( self.dom0, @@ -1981,14 +1979,14 @@ def test_013_feature_set_dom0(self): self.assertEqual(os.path.exists(service_path), True) def test_014_feature_delete_dom0(self): - self.test_base_dir = "/tmp/qubes-test-dir" - self.base_dir_patch = mock.patch.dict( - qubes.config.system_path, {"dom0_services_dir": self.test_base_dir} + test_base_dir = "/tmp/qubes-test-dir" + base_dir_patch = mock.patch.dict( + qubes.config.system_path, {"dom0_services_dir": test_base_dir} ) - self.base_dir_patch.start() - self.addCleanup(self.base_dir_patch.stop) + base_dir_patch.start() + self.addCleanup(base_dir_patch.stop) service = "guivm-gui-agent" - service_path = self.test_base_dir + "/" + service + service_path = test_base_dir + "/" + service self.ext.on_domain_feature_set( self.dom0, @@ -2006,14 +2004,14 @@ def test_014_feature_delete_dom0(self): self.assertEqual(os.path.exists(service_path), False) def test_014_feature_set_empty_value_dom0(self): - self.test_base_dir = "/tmp/qubes-test-dir" - self.base_dir_patch = mock.patch.dict( - qubes.config.system_path, {"dom0_services_dir": self.test_base_dir} + test_base_dir = "/tmp/qubes-test-dir" + base_dir_patch = mock.patch.dict( + qubes.config.system_path, {"dom0_services_dir": test_base_dir} ) - self.base_dir_patch.start() - self.addCleanup(self.base_dir_patch.stop) + base_dir_patch.start() + self.addCleanup(base_dir_patch.stop) service = "guivm-gui-agent" - service_path = self.test_base_dir + "/" + service + service_path = test_base_dir + "/" + service self.ext.on_domain_feature_set( self.dom0, @@ -2631,11 +2629,9 @@ def test_000_shutdown(self): ) def test_000_shutdown_used(self): - with unittest.mock.patch.object( + with mock.patch.object( self.ext, "attached_vms", return_value=[self.client] - ), unittest.mock.patch.object( - self.client, "is_running", return_value=True - ): + ), mock.patch.object(self.client, "is_running", return_value=True): self.client.is_preload = False with self.assertRaises(qubes.exc.QubesVMInUseError): self.ext.on_domain_pre_shutdown( @@ -2650,13 +2646,13 @@ def test_000_shutdown_used(self): ) def test_000_shutdown_used_by_some(self): - with unittest.mock.patch.object( + with mock.patch.object( self.ext, "attached_vms", return_value=[self.client, self.client_alt], - ), unittest.mock.patch.object( + ), mock.patch.object( self.client, "is_running", return_value=True - ), unittest.mock.patch.object( + ), mock.patch.object( self.client_alt, "is_running", return_value=True ): self.client.is_preload = False diff --git a/qubes/tests/integ/dispvm.py b/qubes/tests/integ/dispvm.py index 62892e898..f06694074 100644 --- a/qubes/tests/integ/dispvm.py +++ b/qubes/tests/integ/dispvm.py @@ -26,7 +26,7 @@ import time import unittest from contextlib import suppress -from distutils import spawn +from shutil import which from unittest.mock import patch, mock_open import asyncio import sys @@ -34,7 +34,6 @@ import qubes.config import qubes.tests -import qubesadmin.exc # nose will duplicate this logger. logger = logging.getLogger(__name__) @@ -49,7 +48,7 @@ class TC_04_DispVM(qubes.tests.SystemTestCase): def setUp(self): - super(TC_04_DispVM, self).setUp() + super().setUp() self.init_default_template() self.disp_base = self.app.add_new_vm( qubes.vm.appvm.AppVM, @@ -73,7 +72,7 @@ def setUp(self): def tearDown(self): self.app.default_dispvm = None - super(TC_04_DispVM, self).tearDown() + super().tearDown() def wait_for_dispvm_destroy(self, dispvm_name: list): timeout = 20 @@ -138,6 +137,7 @@ def test_003_cleanup_destroyed(self): self.assertNotIn(dispvm_name, self.app.domains) def _count_dispvms(self, *args, **kwargs): + # pylint: disable=unused-argument self.startup_counter += 1 def test_010_failed_start(self): @@ -195,10 +195,10 @@ def test_011_failed_start_timeout(self): self.assertEqual(self.startup_counter, 1) -class TC_20_DispVMMixin(object): +class TC_20_DispVMMixin: def setUp(self): # pylint: disable=invalid-name logger.info("start") - super(TC_20_DispVMMixin, self).setUp() + super().setUp() if "whonix-g" in self.template: self.skipTest( "whonix gateway is not supported as DisposableVM Template" @@ -259,7 +259,7 @@ def tearDown(self): # pylint: disable=invalid-name if "_preload_" not in self._testMethodName: self.app.default_dispvm = None self.app.save() - super(TC_20_DispVMMixin, self).tearDown() + super().tearDown() logger.info("end") def _test_event_handler( @@ -787,9 +787,7 @@ async def _test_019_preload_refresh(self): self.log_preload() logger.info("end") - @unittest.skipUnless( - spawn.find_executable("xdotool"), "xdotool not installed" - ) + @unittest.skipUnless(which("xdotool"), "xdotool not installed") def test_020_gui_app(self): dispvm = self.loop.run_until_complete( qubes.vm.dispvm.DispVM.from_appvm(self.disp_base) @@ -807,8 +805,8 @@ def test_020_gui_app(self): # wait for DispVM startup: p.stdin.write(b"echo test\n") self.loop.run_until_complete(p.stdin.drain()) - l = self.loop.run_until_complete(p.stdout.readline()) - self.assertEqual(l, b"test\n") + line = self.loop.run_until_complete(p.stdout.readline()) + self.assertEqual(line, b"test\n") self.assertTrue(dispvm.is_running()) try: @@ -849,9 +847,10 @@ def test_020_gui_app(self): ) def _handle_editor(self, winid, copy=False): - (window_title, _) = subprocess.Popen( + with subprocess.Popen( ["xdotool", "getwindowname", winid], stdout=subprocess.PIPE - ).communicate() + ) as proc: + (window_title, _) = proc.communicate() window_title = ( window_title.decode() .strip() @@ -861,7 +860,7 @@ def _handle_editor(self, winid, copy=False): time.sleep(1) if "LibreOffice" in window_title: # wait for actual editor (we've got splash screen) - search = subprocess.Popen( + with subprocess.Popen( [ "xdotool", "search", @@ -873,11 +872,11 @@ def _handle_editor(self, winid, copy=False): "disp*|Writer", ], stdout=subprocess.PIPE, - stderr=open(os.path.devnull, "w"), - ) - retcode = search.wait() - if retcode == 0: - winid = search.stdout.read().strip() + stderr=subprocess.DEVNULL, + ) as search: + retcode = search.wait() + if retcode == 0: + winid = search.stdout.read().strip() time.sleep(0.5) subprocess.check_call( ["xdotool", "windowactivate", "--sync", winid] @@ -929,24 +928,25 @@ def _handle_editor(self, winid, copy=False): ) if copy: raise NotImplementedError("copy not implemented for vim") - else: - subprocess.check_call( - ["xdotool", "key", "i", "type", "Test test 2"] - ) - subprocess.check_call( - ["xdotool", "key", "--window", winid, "key", "Return"] - ) - subprocess.check_call( - ["xdotool", "key", "Escape", "colon", "w", "q", "Return"] - ) - elif ( - "gedit" in window_title - or "KWrite" in window_title - or "Mousepad" in window_title - or "Geany" in window_title - or "Text Editor" in window_title - # FeatherPad (default in Whonix 18), no app name in the title... - or "test.txt" in window_title + subprocess.check_call( + ["xdotool", "key", "i", "type", "Test test 2"] + ) + subprocess.check_call( + ["xdotool", "key", "--window", winid, "key", "Return"] + ) + subprocess.check_call( + ["xdotool", "key", "Escape", "colon", "w", "q", "Return"] + ) + elif any( + e in window_title + for e in ( + "gedit", + "KWrite", + "Mousepad", + "Geany", + "Text Editor", + "test.txt", + ) ): subprocess.check_call( ["xdotool", "windowactivate", "--sync", winid] @@ -984,7 +984,7 @@ def _whonix_ws_dispvm_confirm(self, action_str): include_tray=False, timeout=5, ) - except Exception: + except Exception: # pylint: disable=broad-exception-caught return ( False, "Failed to find qrexec confirmation window for " @@ -1011,9 +1011,7 @@ def _whonix_ws_dispvm_confirm(self, action_str): ) return (True, "") - @unittest.skipUnless( - spawn.find_executable("xdotool"), "xdotool not installed" - ) + @unittest.skipUnless(which("xdotool"), "xdotool not installed") def test_030_edit_file(self): self.testvm1 = self.app.add_new_vm( qubes.vm.appvm.AppVM, @@ -1052,18 +1050,17 @@ def test_030_edit_file(self): include_tray=False, timeout=60, ) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught try: self.loop.run_until_complete(asyncio.wait_for(p.wait(), 1)) except asyncio.TimeoutError: raise e - else: - stdout = self.loop.run_until_complete(p.stdout.read()) - self.fail( - "qvm-open-in-dvm exited prematurely with {}: {}".format( - p.returncode, stdout - ) + stdout = self.loop.run_until_complete(p.stdout.read()) + self.fail( + "qvm-open-in-dvm exited prematurely with {}: {}".format( + p.returncode, stdout ) + ) # let the application initialize self.loop.run_until_complete(asyncio.sleep(1)) try: @@ -1106,8 +1103,8 @@ def _get_open_script(self, application): "/usr/share/qubes/tests-data/" "dispvm-open-thunderbird-attachment", "rb", - ) as f: - return f.read() + ) as file: + return file.read() assert False def _get_apps_list(self, template): @@ -1130,9 +1127,7 @@ def _get_apps_list(self, template): if l.endswith(".desktop") ] - @unittest.skipUnless( - spawn.find_executable("xdotool"), "xdotool not installed" - ) + @unittest.skipUnless(which("xdotool"), "xdotool not installed") def test_100_open_in_dispvm(self): if "whonix-w" in self.template: self.skipTest( @@ -1197,7 +1192,7 @@ def test_100_open_in_dispvm(self): self.loop.run_until_complete(asyncio.sleep(3)) try: - click_to_open = self.loop.run_until_complete( + self.loop.run_until_complete( self.testvm1.run_for_stdio( "./open-file test.txt", stdout=subprocess.PIPE, @@ -1211,7 +1206,7 @@ def test_100_open_in_dispvm(self): self.skipTest("{} not installed".format(app_id)) self.fail( "'./open-file test.txt' failed with {}: {}{}".format( - err.cmd, err.returncode, err.stdout, err.stderr + err.returncode, err.stdout, err.stderr ) ) @@ -1241,8 +1236,8 @@ def test_100_open_in_dispvm(self): self.wait_for_window_hide_coro("editor", winid) ) - with open("/var/run/qubes/qubes-clipboard.bin", "rb") as f: - test_txt_content = f.read() + with open("/var/run/qubes/qubes-clipboard.bin", "rb") as file: + test_txt_content = file.read() self.assertEqual(test_txt_content.strip(), b"test1") # this doesn't really close the application, only the qrexec-client @@ -1260,7 +1255,7 @@ def create_testcases_for_templates(): ) -def load_tests(loader, tests, pattern): +def load_tests(loader, tests, _pattern): tests.addTests(loader.loadTestsFromNames(create_testcases_for_templates())) return tests diff --git a/qubes/tests/integ/network.py b/qubes/tests/integ/network.py index d114e9e11..2c97f0798 100644 --- a/qubes/tests/integ/network.py +++ b/qubes/tests/integ/network.py @@ -19,7 +19,7 @@ # License along with this library; if not, see . # import contextlib -from distutils import spawn +from shutil import which import asyncio import subprocess @@ -35,7 +35,7 @@ # noinspection PyAttributeOutsideInit,PyPep8Naming @qubes.tests.skipIfTemplate("whonix") -class VmNetworkingMixin(object): +class VmNetworkingMixin: test_ip = "192.168.123.45" test_name = "test.example.com" @@ -80,7 +80,7 @@ def setUp(self): """ :type self: qubes.tests.SystemTestCase | VmNetworkingMixin """ - super(VmNetworkingMixin, self).setUp() + super().setUp() if self.template.startswith("whonix-"): self.skipTest( "Test not supported here - Whonix uses its own " @@ -163,7 +163,7 @@ def tearDown(self): ) self._run_cmd_and_log_output(self.app.domains[0], "xl list") del self.netvms - super(VmNetworkingMixin, self).tearDown() + super().tearDown() def configure_netvm(self): """ @@ -211,11 +211,10 @@ def run_netvm_cmd(qube, cmd): ip=self.test_ip, name=self.test_name ), ) + cmd = "rm -f /etc/resolv.conf && echo nameserver" run_netvm_cmd( qube, - "rm -f /etc/resolv.conf && echo nameserver {} > /etc/resolv.conf".format( - self.test_ip - ), + "{} {} > /etc/resolv.conf".format(cmd, self.test_ip), ) run_netvm_cmd(qube, "systemctl try-restart systemd-resolved || :") run_netvm_cmd(qube, "/usr/lib/qubes/qubes-setup-dnat-to-ns") @@ -474,9 +473,7 @@ def test_010_simple_proxyvm(self): ) @qubes.tests.expectedFailureIfTemplate("debian-7") - @unittest.skipUnless( - spawn.find_executable("xdotool"), "xdotool not installed" - ) + @unittest.skipUnless(which("xdotool"), "xdotool not installed") def test_020_simple_proxyvm_nm(self): """ :type self: qubes.tests.SystemTestCase | VmNetworkingMixin @@ -1157,9 +1154,8 @@ def test_203_fake_ip_inter_vm_allow(self): self.loop.run_until_complete(self.start_vm(self.testvm1)) self.loop.run_until_complete(self.start_vm(self.testvm2)) - cmd = "nft add ip qubes custom-forward ip saddr {} ip daddr {} accept".format( - self.testvm2.ip, self.testvm1.ip - ) + cmd = "nft add ip qubes custom-forward ip saddr " + cmd += "{} ip daddr {} accept".format(self.testvm2.ip, self.testvm1.ip) try: self.loop.run_until_complete( self.proxy.run_for_stdio(cmd, user="root") @@ -1170,9 +1166,8 @@ def test_203_fake_ip_inter_vm_allow(self): ) from None try: - cmd = "nft add ip qubes custom-input ip saddr {} counter accept".format( - self.testvm2.ip - ) + cmd = "nft add ip qubes custom-input ip saddr " + cmd += "{} counter accept".format(self.testvm2.ip) self.loop.run_until_complete( self.testvm1.run_for_stdio(cmd, user="root") ) @@ -1369,7 +1364,7 @@ def create_testcases_for_templates(): ) -def load_tests(loader, tests, pattern): +def load_tests(loader, tests, _pattern): tests.addTests(loader.loadTestsFromNames(create_testcases_for_templates())) return tests diff --git a/qubes/tests/integ/network_ipv6.py b/qubes/tests/integ/network_ipv6.py index 646638acb..6665fc17e 100644 --- a/qubes/tests/integ/network_ipv6.py +++ b/qubes/tests/integ/network_ipv6.py @@ -24,7 +24,7 @@ import sys import time import unittest -from distutils import spawn +from shutil import which import qubes.firewall import qubes.tests @@ -46,7 +46,7 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin): ) def setUp(self): - super(VmIPv6NetworkingMixin, self).setUp() + super().setUp() self.ping6_ip = self.ping6_cmd.format(target=self.test_ip6) self.ping6_name = self.ping6_cmd.format(target=self.test_name) self.ping6_deadline_ip = self.ping6_deadline_cmd.format( @@ -92,7 +92,7 @@ def run_netvm_cmd(qube, cmd): for qube in self.netvms: qube.features["ipv6"] = True - super(VmIPv6NetworkingMixin, self).configure_netvm() + super().configure_netvm() for qube in self.netvms: run_netvm_cmd( @@ -106,12 +106,11 @@ def run_netvm_cmd(qube, cmd): ) # ignore failure self.run_cmd(qube, "while pkill dnsmasq; do sleep 1; done") - run_netvm_cmd( - qube, - "dnsmasq -a {ip} -A /{name}/{ip} -A /{name}/{ip6} -i test0 -z".format( - ip=self.test_ip, ip6=self.test_ip6, name=self.test_name - ), + dnsmasq_args = "-a {ip} -A /{name}/{ip} -A /{name}/{ip6}".format( + ip=self.test_ip, ip6=self.test_ip6, name=self.test_name ) + dnsmasq_cmd = "dnsmasq {} -i test0 -z".format(dnsmasq_args) + run_netvm_cmd(qube, dnsmasq_cmd) def test_500_ipv6_simple_networking(self): """ @@ -204,9 +203,7 @@ def test_510_ipv6_simple_proxyvm(self): ) @qubes.tests.expectedFailureIfTemplate("debian-7") - @unittest.skipUnless( - spawn.find_executable("xdotool"), "xdotool not installed" - ) + @unittest.skipUnless(which("xdotool"), "xdotool not installed") def test_520_ipv6_simple_proxyvm_nm(self): """ :type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin @@ -533,13 +530,10 @@ def test_550_ipv6_spoof_ip(self): ) retcode = self.run_cmd(self.testnetvm, cmd) if retcode == 127: + ip6_args = "! -s {} -p icmpv6 -j LOG".format(self.testvm1.ip6) + ip6_cmd = "ip6tables -I INPUT -i vif+ {}".format(ip6_args) self.assertEqual( - self.run_cmd( - self.testnetvm, - "ip6tables -I INPUT -i vif+ ! -s {} -p icmpv6 -j LOG".format( - self.testvm1.ip6 - ), - ), + self.run_cmd(self.testnetvm, ip6_cmd), 0, ) iptables = True @@ -690,7 +684,7 @@ def create_testcases_for_templates(): ) -def load_tests(loader, tests, pattern): +def load_tests(loader, tests, _pattern): tests.addTests(loader.loadTestsFromNames(create_testcases_for_templates())) return tests diff --git a/qubes/tests/vm/adminvm.py b/qubes/tests/vm/adminvm.py index 55734ef8d..5d16105c5 100644 --- a/qubes/tests/vm/adminvm.py +++ b/qubes/tests/vm/adminvm.py @@ -23,10 +23,6 @@ import unittest import unittest.mock -import functools - -import asyncio - import qubes import qubes.exc import qubes.vm @@ -69,7 +65,7 @@ def tearDown(self) -> None: del self.emitter try: os.unlink("/tmp/qubestest.xml") - except: + except: # pylint: disable=bare-except pass super().tearDown() @@ -121,8 +117,8 @@ def test_311_suspend(self): def test_700_run_service(self, mock_subprocess): # if there is a user in 'qubes' group, it should be used by default try: - gr = grp.getgrnam("qubes") - default_user = gr.gr_mem[0] + group = grp.getgrnam("qubes") + default_user = group.gr_mem[0] command_prefix = ["runuser", "-u", default_user, "--"] except (KeyError, IndexError): command_prefix = [] diff --git a/qubes/tests/vm/appvm.py b/qubes/tests/vm/appvm.py index adac199c1..05e1639f5 100644 --- a/qubes/tests/vm/appvm.py +++ b/qubes/tests/vm/appvm.py @@ -29,19 +29,20 @@ import qubes.vm.templatevm -class TestApp(object): +class TestApp: + # pylint: disable=too-few-public-methods labels = {1: qubes.Label(1, "0xcc0000", "red")} def __init__(self): self.domains = {} -class TestProp(object): +class TestProp: # pylint: disable=too-few-public-methods __name__ = "testprop" -class TestVM(object): +class TestVM: # pylint: disable=too-few-public-methods app = TestApp() @@ -56,13 +57,13 @@ def is_running(self): class TestVolume(qubes.storage.Volume): - def create(self): + def create(self): # pylint: disable=invalid-overridden-method pass class TestPool(qubes.storage.Pool): def __init__(self, *args, **kwargs): - super(TestPool, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._volumes = {} def init_volume(self, vm, volume_config): @@ -98,7 +99,7 @@ def cleanup_appvm(self): self.app.domains.clear() self.app.pools.clear() - def get_vm(self, **kwargs): + def get_vm(self, *_args, **kwargs): vm = qubes.vm.appvm.AppVM( self.app, None, @@ -189,13 +190,13 @@ def test_500_property_migrate_template_for_dispvms(self): vm = qubes.vm.appvm.AppVM(self.app, xml) self.assertEqual(vm.template_for_dispvms, True) with self.assertRaises(AttributeError): - vm.dispvm_allowed + vm.dispvm_allowed # pylint: disable=no-member,pointless-statement xml = lxml.etree.XML(xml_template.format(value="False")) vm = qubes.vm.appvm.AppVM(self.app, xml) self.assertEqual(vm.template_for_dispvms, False) with self.assertRaises(AttributeError): - vm.dispvm_allowed + vm.dispvm_allowed # pylint: disable=no-member,pointless-statement def test_600_load_volume_config(self): xml_template = """ diff --git a/qubes/tests/vm/dispvm.py b/qubes/tests/vm/dispvm.py index 7644f5fb7..281595145 100644 --- a/qubes/tests/vm/dispvm.py +++ b/qubes/tests/vm/dispvm.py @@ -19,7 +19,7 @@ # License along with this library; if not, see . import unittest -import unittest.mock as mock +from unittest import mock import asyncio @@ -34,7 +34,7 @@ class TestApp(qubes.tests.vm.TestApp): def __init__(self): - super(TestApp, self).__init__() + super().__init__() self.qid_counter = 0 def add_new_vm(self, cls, **kwargs): @@ -52,7 +52,7 @@ def add_new_vm(self, cls, **kwargs): class TC_00_DispVM(qubes.tests.QubesTestCase): def setUp(self): - super(TC_00_DispVM, self).setUp() + super().setUp() self.app = TestApp() self.app.save = mock.Mock() self.app.pools["default"] = qubes.tests.vm.appvm.TestPool( @@ -85,7 +85,7 @@ def setUp(self): def tearDown(self): del self.emitter - super(TC_00_DispVM, self).tearDown() + super().tearDown() def cleanup_dispvm(self): if hasattr(self, "dispvm"): @@ -335,7 +335,7 @@ def test_000_get_preload_templates(self): def test_001_from_appvm_reject_not_allowed(self): with self.assertRaises(qubes.exc.QubesException): - dispvm = self.loop.run_until_complete( + self.loop.run_until_complete( qubes.vm.dispvm.DispVM.from_appvm(self.appvm) ) @@ -352,20 +352,20 @@ def test_002_template_change(self): "__getitem__.side_effect": orig_getitem, } ) - self.dispvm = self.app.add_new_vm( + dispvm = self.app.add_new_vm( qubes.vm.dispvm.DispVM, name="test-dispvm", template=self.appvm ) - self.dispvm.template = self.appvm - self.loop.run_until_complete(self.dispvm.start()) + dispvm.template = self.appvm + self.loop.run_until_complete(dispvm.start()) if not self.app.vmm.offline_mode: assert not dispvm.is_halted() with self.assertRaises(qubes.exc.QubesVMNotHaltedError): - self.dispvm.template = self.appvm + dispvm.template = self.appvm with self.assertRaises(qubes.exc.QubesValueError): - self.dispvm.template = qubes.property.DEFAULT - self.loop.run_until_complete(self.dispvm.kill()) - self.dispvm.template = self.appvm + dispvm.template = qubes.property.DEFAULT + self.loop.run_until_complete(dispvm.kill()) + dispvm.template = self.appvm def test_003_dvmtemplate_template_change(self): self.appvm.template_for_dispvms = True @@ -381,7 +381,7 @@ def test_003_dvmtemplate_template_change(self): "__setitem__.side_effect": orig_domains.__setitem__, } ) - self.dispvm = self.app.add_new_vm( + self.app.add_new_vm( qubes.vm.dispvm.DispVM, name="test-dispvm", template=self.appvm ) @@ -403,7 +403,7 @@ def test_004_dvmtemplate_allowed_change(self): "__setitem__.side_effect": orig_domains.__setitem__, } ) - self.dispvm = self.app.add_new_vm( + self.app.add_new_vm( qubes.vm.dispvm.DispVM, name="test-dispvm", template=self.appvm ) @@ -422,11 +422,10 @@ def test_010_create_direct(self): "__getitem__.side_effect": orig_getitem, } ) - self.dispvm = self.app.add_new_vm( + dispvm = self.app.add_new_vm( qubes.vm.dispvm.DispVM, name="test-dispvm", template=self.appvm ) mock_domains.get_new_unused_dispid.assert_called_once_with() - dispvm = self.dispvm self.assertEqual(dispvm.name, "test-dispvm") self.assertEqual(dispvm.template, self.appvm) self.assertEqual(dispvm.label, self.appvm.label) @@ -475,7 +474,7 @@ def test_011_create_direct_reject(self): @mock.patch("os.symlink") @mock.patch("os.makedirs") - def test_020_copy_storage_pool(self, mock_makedirs, mock_symlink): + def test_020_copy_storage_pool(self, _mock_makedirs, _mock_symlink): self.app.pools["alternative"] = qubes.tests.vm.appvm.TestPool( name="alternative" ) @@ -525,11 +524,11 @@ def test_021_storage_template_change(self): "__setitem__.side_effect": orig_domains.__setitem__, } ) - vm = self.dispvm = self.app.add_new_vm( + dispvm = self.app.add_new_vm( qubes.vm.dispvm.DispVM, name="test-dispvm", template=self.appvm ) self.assertIs( - vm.volume_config["root"]["source"], + dispvm.volume_config["root"]["source"], self.template.volumes["root"], ) # create new mock, so new template will get different volumes @@ -543,20 +542,21 @@ def test_021_storage_template_change(self): self.app.domains[template2] = template2 self.appvm.template = template2 - self.assertFalse(vm.volume_config["root"]["save_on_stop"]) - self.assertTrue(vm.volume_config["root"]["snap_on_start"]) + self.assertFalse(dispvm.volume_config["root"]["save_on_stop"]) + self.assertTrue(dispvm.volume_config["root"]["snap_on_start"]) self.assertNotEqual( - vm.volume_config["root"]["source"], self.template.volumes["root"] + dispvm.volume_config["root"]["source"], + self.template.volumes["root"], ) self.assertIs( - vm.volume_config["root"]["source"], template2.volumes["root"] + dispvm.volume_config["root"]["source"], template2.volumes["root"] ) self.assertIs( - vm.volume_config["root"]["source"], + dispvm.volume_config["root"]["source"], self.appvm.volume_config["root"]["source"], ) self.assertIs( - vm.volume_config["private"]["source"], + dispvm.volume_config["private"]["source"], self.appvm.volumes["private"], ) @@ -575,7 +575,7 @@ def test_022_storage_app_change(self): "__setitem__.side_effect": orig_domains.__setitem__, } ) - vm = self.dispvm = self.app.add_new_vm( + vm = dispvm = self.app.add_new_vm( qubes.vm.dispvm.DispVM, name="test-dispvm", template=self.appvm ) self.assertTrue(vm.events_enabled) @@ -600,9 +600,9 @@ def test_022_storage_app_change(self): app2.template_for_dispvms = True self.app.domains[app2.name] = app2 self.app.domains[app2] = app2 - self.dispvm.template = app2 + dispvm.template = app2 - self.assertIs(vm, self.dispvm) + self.assertIs(vm, dispvm) self.assertFalse(vm.volume_config["root"]["save_on_stop"]) self.assertTrue(vm.volume_config["root"]["snap_on_start"]) self.assertFalse(vm.volume_config["private"]["save_on_stop"]) @@ -630,7 +630,7 @@ def test_022_storage_app_change(self): @mock.patch("os.symlink") @mock.patch("os.makedirs") - def test_023_inherit_ephemeral(self, mock_makedirs, mock_symlink): + def test_023_inherit_ephemeral(self, _mock_makedirs, _mock_symlink): self.app.pools["alternative"] = qubes.tests.vm.appvm.TestPool( name="alternative" ) diff --git a/qubes/tests/vm/mix/dvmtemplate.py b/qubes/tests/vm/mix/dvmtemplate.py index 154473fdc..238e877cf 100755 --- a/qubes/tests/vm/mix/dvmtemplate.py +++ b/qubes/tests/vm/mix/dvmtemplate.py @@ -33,7 +33,7 @@ class TestApp(qubes.tests.vm.TestApp): def __init__(self): - super(TestApp, self).__init__() + super().__init__() self.qid_counter = 0 def add_new_vm(self, cls, **kwargs): @@ -54,7 +54,7 @@ class TC_00_DVMTemplateMixin( qubes.tests.QubesTestCase, ): def setUp(self): - super(TC_00_DVMTemplateMixin, self).setUp() + super().setUp() self.app = TestApp() self.app.save = mock.Mock() self.app.pools["default"] = qubes.tests.vm.appvm.TestPool( @@ -93,7 +93,7 @@ def setUp(self): def tearDown(self): self.app.default_dispvm = None del self.emitter - super(TC_00_DVMTemplateMixin, self).tearDown() + super().tearDown() def cleanup_adminvm(self): self.adminvm.close() diff --git a/qubes/tests/vm/mix/net.py b/qubes/tests/vm/mix/net.py index 1d286262f..f28ce98ae 100644 --- a/qubes/tests/vm/mix/net.py +++ b/qubes/tests/vm/mix/net.py @@ -20,7 +20,6 @@ # License along with this library; if not, see . # import ipaddress -import unittest from unittest.mock import patch import qubes @@ -35,7 +34,7 @@ class TC_00_NetVMMixin( qubes.tests.vm.qubesvm.QubesVMTestsMixin, qubes.tests.QubesTestCase ): def setUp(self): - super(TC_00_NetVMMixin, self).setUp() + super().setUp() self.app = qubes.tests.vm.TestApp() self.app.vmm.offline_mode = True diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index 50bfa1821..2af1763b3 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -29,9 +29,6 @@ from uuid import UUID import datetime -import asyncio - -import functools import lxml.etree import unittest.mock diff --git a/qubes/vm/appvm.py b/qubes/vm/appvm.py index f046d05d3..0089a1f6d 100644 --- a/qubes/vm/appvm.py +++ b/qubes/vm/appvm.py @@ -114,6 +114,7 @@ def __init__(self, app, xml, **kwargs): @qubes.stateless_property def icon(self): if self.template_for_dispvms: + # pylint: disable=no-member return "templatevm-" + self.label.name # multi-inheritance and properties confuses pylint here # pylint: disable=no-member From cf67d5d5a06e5200251317feea1ecef3bd0aa1a6 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 6 Nov 2025 12:42:26 +0100 Subject: [PATCH 5/8] Add numbers of preload disp perf for GUI calls For: https://github.com/QubesOS/qubes-issues/issues/1512 --- qubes/tests/integ/dispvm.py | 2 +- qubes/tests/integ/dispvm_perf.py | 222 ++++++++++++++++++++++++------- tests/dispvm_perf.py | 102 ++++++++++---- tests/dispvm_perf_reader.py | 196 ++++++++++++++++++--------- 4 files changed, 385 insertions(+), 137 deletions(-) diff --git a/qubes/tests/integ/dispvm.py b/qubes/tests/integ/dispvm.py index f06694074..2ac559b03 100644 --- a/qubes/tests/integ/dispvm.py +++ b/qubes/tests/integ/dispvm.py @@ -227,11 +227,11 @@ def setUp(self): # pylint: disable=invalid-name self.start_vm(self.disp_base), self.start_vm(self.disp_base_alt), ] + self.loop.run_until_complete(asyncio.gather(*start_tasks)) shutdown_tasks = [ self.disp_base.shutdown(wait=True), self.disp_base_alt.shutdown(wait=True), ] - self.loop.run_until_complete(asyncio.gather(*start_tasks)) self.loop.run_until_complete(asyncio.gather(*shutdown_tasks)) # Setting "default_dispvm" fires the preload event before patches of # each test function is applied. diff --git a/qubes/tests/integ/dispvm_perf.py b/qubes/tests/integ/dispvm_perf.py index 5f72a0635..dce1d2715 100644 --- a/qubes/tests/integ/dispvm_perf.py +++ b/qubes/tests/integ/dispvm_perf.py @@ -21,10 +21,27 @@ import shutil import sys import tempfile - +import unittest import qubes.tests +def env_with_reason(env_var, default): + env_var = "QUBES_TEST_PERF_" + env_var + value = os.environ.get(env_var, default) + reason = "environment variable is empty: " + env_var + return value, reason + + +CONCURRENCY = env_with_reason("CONCURRENCY", False) +GUI = env_with_reason("GUI", True) +EXTENDED = env_with_reason("EXTENDED", False) +DOM0_DISPVM = env_with_reason("DOM0_DISPVM", False) +DOM0_DISPVM_API = env_with_reason("DOM0_DISPVM_API", True) +DOM0_VM_API = env_with_reason("DOM0_VM_API", True) +VM_DISPVM = env_with_reason("VM_DISPVM", False) +VM_VM = env_with_reason("VM_VM", False) + + class TC_00_DispVMPerfMixin: def setUp(self: qubes.tests.SystemTestCase): super().setUp() @@ -129,147 +146,256 @@ def run_reader(self, args: list): if p.returncode: self.fail(f"'{' '.join(cmd)}' failed: {p.returncode}") + @unittest.skipUnless(*VM_DISPVM) def test_000_vm_dispvm(self): """Latency of vm-dispvm calls""" self.run_test("vm-dispvm") + @unittest.skipUnless(*VM_DISPVM) + @unittest.skipUnless(*GUI) def test_001_vm_dispvm_gui(self): """Latency of vm-dispvm GUI calls""" self.run_test("vm-dispvm-gui") + @unittest.skipUnless(*VM_DISPVM) + @unittest.skipUnless(*CONCURRENCY) def test_002_vm_dispvm_concurrent(self): """Latency of vm-dispvm concurrent calls""" self.run_test("vm-dispvm-concurrent") + @unittest.skipUnless(*VM_DISPVM) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*CONCURRENCY) def test_003_vm_dispvm_gui_concurrent(self): """Latency of vm-dispvm concurrent GUI calls""" self.run_test("vm-dispvm-gui-concurrent") - def test_006_dom0_dispvm(self): + @unittest.skipUnless(*DOM0_DISPVM) + def test_100_dom0_dispvm(self): """Latency of dom0-dispvm calls""" self.run_test("dom0-dispvm") - def test_007_dom0_dispvm_gui(self): + @unittest.skipUnless(*DOM0_DISPVM) + @unittest.skipUnless(*GUI) + def test_101_dom0_dispvm_gui(self): """Latency of dom0-dispvm GUI calls""" self.run_test("dom0-dispvm-gui") - def test_008_dom0_dispvm_concurrent(self): + @unittest.skipUnless(*DOM0_DISPVM) + @unittest.skipUnless(*CONCURRENCY) + def test_102_dom0_dispvm_concurrent(self): """Latency of dom0-dispvm concurrent calls""" self.run_test("dom0-dispvm-concurrent") - def test_009_dom0_dispvm_gui_concurrent(self): + @unittest.skipUnless(*DOM0_DISPVM) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*CONCURRENCY) + def test_103_dom0_dispvm_gui_concurrent(self): """Latency of dom0-dispvm concurrent GUI calls""" self.run_test("dom0-dispvm-gui-concurrent") - def test_020_vm_dispvm_preload(self): + @unittest.skipUnless(*VM_DISPVM) + def test_200_vm_dispvm_preload(self): """Latency of vm-dispvm (preload) calls""" self.run_test("vm-dispvm-preload") - def test_021_vm_dispvm_preload_gui(self): + @unittest.skipUnless(*VM_DISPVM) + @unittest.skipUnless(*GUI) + def test_201_vm_dispvm_preload_gui(self): """Latency of vm-dispvm (preload) GUI calls""" self.run_test("vm-dispvm-preload-gui") - def test_022_vm_dispvm_preload_concurrent(self): + @unittest.skipUnless(*VM_DISPVM) + @unittest.skipUnless(*CONCURRENCY) + def test_202_vm_dispvm_preload_concurrent(self): """Latency of vm-dispvm (preload) concurrent calls""" self.run_test("vm-dispvm-preload-concurrent") - def test_023_vm_dispvm_preload_gui_concurrent(self): + @unittest.skipUnless(*VM_DISPVM) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*CONCURRENCY) + def test_203_vm_dispvm_preload_gui_concurrent(self): """Latency of vm-dispvm (preload) concurrent GUI calls""" self.run_test("vm-dispvm-preload-gui-concurrent") - def test_026_dom0_dispvm_preload(self): + @unittest.skipUnless(*DOM0_DISPVM) + def test_300_dom0_dispvm_preload(self): """Latency of dom0-dispvm (preload) calls""" self.run_test("dom0-dispvm-preload") - def test_027_dom0_dispvm_preload_gui(self): + @unittest.skipUnless(*DOM0_DISPVM) + @unittest.skipUnless(*GUI) + def test_301_dom0_dispvm_preload_gui(self): """Latency of dom0-dispvm (preload) GUI calls""" self.run_test("dom0-dispvm-preload-gui") - def test_028_dom0_dispvm_preload_concurrent(self): + @unittest.skipUnless(*DOM0_DISPVM) + @unittest.skipUnless(*CONCURRENCY) + def test_302_dom0_dispvm_preload_concurrent(self): """Latency of dom0-dispvm (preload) concurrent calls""" self.run_test("dom0-dispvm-preload-concurrent") - def test_029_dom0_dispvm_preload_gui_concurrent(self): + @unittest.skipUnless(*DOM0_DISPVM) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*CONCURRENCY) + def test_303_dom0_dispvm_preload_gui_concurrent(self): """Latency of dom0-dispvm (preload) concurrent GUI calls""" self.run_test("dom0-dispvm-preload-gui-concurrent") + @unittest.skipUnless(*DOM0_DISPVM_API) def test_400_dom0_dispvm_api(self): """Latency of dom0-dispvm API calls""" self.run_test("dom0-dispvm-api") + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*GUI) def test_401_dom0_dispvm_gui_api(self): """Latency of dom0-dispvm GUI API calls""" self.run_test("dom0-dispvm-gui-api") + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*CONCURRENCY) def test_402_dom0_dispvm_concurrent_api(self): """Latency of dom0-dispvm concurrent API calls""" self.run_test("dom0-dispvm-concurrent-api") + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*CONCURRENCY) def test_403_dom0_dispvm_gui_concurrent_api(self): """Latency of dom0-dispvm concurrent GUI API calls""" self.run_test("dom0-dispvm-gui-concurrent-api") - def test_404_dom0_dispvm_preload_less_less_api(self): - """Latency of dom0-dispvm (preload less less) API calls""" - self.run_test("dom0-dispvm-preload-less-less-api") - - def test_405_dom0_dispvm_preload_less_api(self): - """Latency of dom0-dispvm (preload less) API calls""" - self.run_test("dom0-dispvm-preload-less-api") - - def test_406_dom0_dispvm_preload_api(self): - """Latency of dom0-dispvm (preload) API calls""" - self.run_test("dom0-dispvm-preload-api") - - def test_407_dom0_dispvm_preload_more_api(self): - """Latency of dom0-dispvm (preload more) API calls""" - self.run_test("dom0-dispvm-preload-more-api") - - def test_408_dom0_dispvm_preload_more_more_api(self): - """Latency of dom0-dispvm (preload more more) API calls""" - self.run_test("dom0-dispvm-preload-more-more-api") - - def test_409_dom0_dispvm_preload_gui_api(self): - """Latency of dom0-dispvm (preload) GUI API calls""" - self.run_test("dom0-dispvm-preload-gui-api") - - def test_410_dom0_dispvm_preload_concurrent_api(self): - """Latency of dom0-dispvm (preload) concurrent GUI API calls""" + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*CONCURRENCY) + def test_404_dom0_dispvm_preload_concurrent_api(self): + """Latency of dom0-dispvm (preload) concurrent API calls""" self.run_test("dom0-dispvm-preload-concurrent-api") - def test_411_dom0_dispvm_preload_gui_concurrent_api(self): + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*CONCURRENCY) + def test_405_dom0_dispvm_preload_gui_concurrent_api(self): """Latency of dom0-dispvm (preload) concurrent GUI API calls""" self.run_test("dom0-dispvm-preload-gui-concurrent-api") - def test_800_vm_vm(self): + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*EXTENDED) + def test_410_dom0_dispvm_preload_1_api(self): + """Latency of dom0-dispvm (1 preload) API calls""" + self.run_test("dom0-dispvm-preload-1-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + def test_411_dom0_dispvm_preload_2_api(self): + """Latency of dom0-dispvm (2 preload) API calls""" + self.run_test("dom0-dispvm-preload-2-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*EXTENDED) + def test_412_dom0_dispvm_preload_3_api(self): + """Latency of dom0-dispvm (3 preload) API calls""" + self.run_test("dom0-dispvm-preload-3-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + def test_413_dom0_dispvm_preload_4_api(self): + """Latency of dom0-dispvm (4 preload) API calls""" + self.run_test("dom0-dispvm-preload-4-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*EXTENDED) + def test_414_dom0_dispvm_preload_5_api(self): + """Latency of dom0-dispvm (5 preload) API calls""" + self.run_test("dom0-dispvm-preload-5-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + def test_415_dom0_dispvm_preload_6_api(self): + """Latency of dom0-dispvm (6 preload) API calls""" + self.run_test("dom0-dispvm-preload-6-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*EXTENDED) + def test_420_dom0_dispvm_preload_1_gui_api(self): + """Latency of dom0-dispvm (1 preload) GUI API calls""" + self.run_test("dom0-dispvm-preload-1-gui-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*GUI) + def test_421_dom0_dispvm_preload_2_gui_api(self): + """Latency of dom0-dispvm (2 preload) GUI API calls""" + self.run_test("dom0-dispvm-preload-2-gui-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*EXTENDED) + def test_422_dom0_dispvm_preload_3_gui_api(self): + """Latency of dom0-dispvm (3 preload) GUI API calls""" + self.run_test("dom0-dispvm-preload-3-gui-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*GUI) + def test_423_dom0_dispvm_preload_4_gui_api(self): + """Latency of dom0-dispvm (4 preload) GUI API calls""" + self.run_test("dom0-dispvm-preload-4-gui-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*EXTENDED) + def test_424_dom0_dispvm_preload_5_gui_api(self): + """Latency of dom0-dispvm (5 preload) GUI API calls""" + self.run_test("dom0-dispvm-preload-5-gui-api") + + @unittest.skipUnless(*DOM0_DISPVM_API) + @unittest.skipUnless(*GUI) + def test_425_dom0_dispvm_preload_6_gui_api(self): + """Latency of dom0-dispvm (6 preload) GUI API calls""" + self.run_test("dom0-dispvm-preload-6-gui-api") + + @unittest.skipUnless(*VM_VM) + def test_700_vm_vm(self): """Latency of vm-vm calls""" self.run_test("vm-vm") - def test_801_vm_vm_gui(self): + @unittest.skipUnless(*VM_VM) + @unittest.skipUnless(*GUI) + def test_701_vm_vm_gui(self): """Latency of vm-vm GUI calls""" self.run_test("vm-vm-gui") - def test_802_vm_vm_concurrent(self): + @unittest.skipUnless(*VM_VM) + @unittest.skipUnless(*CONCURRENCY) + def test_702_vm_vm_concurrent(self): """Latency of vm-vm concurrent calls""" self.run_test("vm-vm-concurrent") - def test_803_vm_vm_gui_concurrent(self): + @unittest.skipUnless(*VM_VM) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*CONCURRENCY) + def test_703_vm_vm_gui_concurrent(self): """Latency of vm-vm concurrent GUI calls""" self.run_test("vm-vm-gui-concurrent") - def test_804_dom0_vm_api(self): + @unittest.skipUnless(*DOM0_VM_API) + def test_800_dom0_vm_api(self): """Latency of dom0-vm API calls""" self.run_test("dom0-vm-api") - def test_805_dom0_vm_gui_api(self): + @unittest.skipUnless(*DOM0_VM_API) + @unittest.skipUnless(*GUI) + def test_801_dom0_vm_gui_api(self): """Latency of dom0-vm GUI API calls""" self.run_test("dom0-vm-gui-api") - def test_806_dom0_vm_concurrent_api(self): + @unittest.skipUnless(*DOM0_VM_API) + @unittest.skipUnless(*CONCURRENCY) + def test_802_dom0_vm_concurrent_api(self): """Latency of dom0-vm concurrent API calls""" self.run_test("dom0-vm-concurrent-api") - def test_807_dom0_vm_gui_concurrent_api(self): + @unittest.skipUnless(*DOM0_VM_API) + @unittest.skipUnless(*GUI) + @unittest.skipUnless(*CONCURRENCY) + def test_803_dom0_vm_gui_concurrent_api(self): """Latency of dom0-vm concurrent GUI API calls""" self.run_test("dom0-vm-gui-concurrent-api") diff --git a/tests/dispvm_perf.py b/tests/dispvm_perf.py index 42eb5a6f9..6ece0f21d 100755 --- a/tests/dispvm_perf.py +++ b/tests/dispvm_perf.py @@ -207,7 +207,7 @@ def __post_init__(self): POLICY_FILE = "/run/qubes/policy.d/10-test-dispvm-perf.policy" # MAX_PRELOAD is the number doesn't overpreload or underpreload (best # performance) on sequential calls between the tests -# "dispvm-preload(-(more|less))-api" (tested on fedora-42-xfce). Machines with +# "dispvm-preload(-NUMBER)-api" (tested on fedora-42-xfce). Machines with # different hardware or domains that boot faster or slower can theoretically # have a different best value. MAX_PRELOAD = 4 @@ -256,12 +256,6 @@ def __post_init__(self): TestConfig("vm-dispvm-gui", gui=True), TestConfig("vm-dispvm-concurrent", concurrent=True), TestConfig("vm-dispvm-gui-concurrent", gui=True, concurrent=True), - TestConfig("dom0-dispvm", from_dom0=True), - TestConfig("dom0-dispvm-gui", gui=True, from_dom0=True), - TestConfig("dom0-dispvm-concurrent", concurrent=True, from_dom0=True), - TestConfig( - "dom0-dispvm-gui-concurrent", gui=True, concurrent=True, from_dom0=True - ), TestConfig("vm-dispvm-preload", preload_max=MAX_PRELOAD), TestConfig("vm-dispvm-preload-gui", gui=True, preload_max=MAX_PRELOAD), TestConfig( @@ -275,6 +269,12 @@ def __post_init__(self): concurrent=True, preload_max=MAX_CONCURRENCY, ), + TestConfig("dom0-dispvm", from_dom0=True), + TestConfig("dom0-dispvm-gui", gui=True, from_dom0=True), + TestConfig("dom0-dispvm-concurrent", concurrent=True, from_dom0=True), + TestConfig( + "dom0-dispvm-gui-concurrent", gui=True, concurrent=True, from_dom0=True + ), TestConfig("dom0-dispvm-preload", from_dom0=True, preload_max=MAX_PRELOAD), TestConfig( "dom0-dispvm-preload-gui", @@ -311,50 +311,97 @@ def __post_init__(self): from_dom0=True, ), TestConfig( - "dom0-dispvm-preload-less-less-api", - preload_max=MAX_PRELOAD - 2, + "dom0-dispvm-preload-1-api", + preload_max=1, admin_api=True, - extra_id="dispvm-preload-api", + extra_id="dom0-dispvm-preload-api", from_dom0=True, ), TestConfig( - "dom0-dispvm-preload-less-api", - preload_max=MAX_PRELOAD - 1, + "dom0-dispvm-preload-1-gui-api", + preload_max=1, + gui=True, admin_api=True, - extra_id="dispvm-preload-api", + extra_id="dom0-dispvm-preload-gui-api", from_dom0=True, ), TestConfig( - "dom0-dispvm-preload-api", - preload_max=MAX_PRELOAD, + "dom0-dispvm-preload-2-api", + preload_max=2, admin_api=True, + extra_id="dom0-dispvm-preload-api", from_dom0=True, ), TestConfig( - "dom0-dispvm-preload-concurrent-api", - concurrent=True, - preload_max=MAX_CONCURRENCY, + "dom0-dispvm-preload-2-gui-api", + preload_max=2, + gui=True, + admin_api=True, + extra_id="dom0-dispvm-preload-gui-api", + from_dom0=True, + ), + TestConfig( + "dom0-dispvm-preload-3-api", + preload_max=3, admin_api=True, + extra_id="dom0-dispvm-preload-api", from_dom0=True, ), TestConfig( - "dom0-dispvm-preload-more-api", - preload_max=MAX_PRELOAD + 1, + "dom0-dispvm-preload-3-gui-api", + preload_max=3, + gui=True, admin_api=True, - extra_id="dispvm-preload-api", + extra_id="dom0-dispvm-preload-gui-api", from_dom0=True, ), TestConfig( - "dom0-dispvm-preload-more-more-api", - preload_max=MAX_PRELOAD + 2, + "dom0-dispvm-preload-4-api", + preload_max=4, admin_api=True, - extra_id="dispvm-preload-api", from_dom0=True, ), TestConfig( - "dom0-dispvm-preload-gui-api", + "dom0-dispvm-preload-4-gui-api", + preload_max=4, gui=True, - preload_max=MAX_PRELOAD, + admin_api=True, + from_dom0=True, + ), + TestConfig( + "dom0-dispvm-preload-5-api", + preload_max=5, + admin_api=True, + extra_id="dom0-dispvm-preload-api", + from_dom0=True, + ), + TestConfig( + "dom0-dispvm-preload-5-gui-api", + preload_max=5, + gui=True, + admin_api=True, + extra_id="dom0-dispvm-preload-gui-api", + from_dom0=True, + ), + TestConfig( + "dom0-dispvm-preload-6-api", + preload_max=6, + admin_api=True, + extra_id="dom0-dispvm-preload-api", + from_dom0=True, + ), + TestConfig( + "dom0-dispvm-preload-6-gui-api", + preload_max=6, + gui=True, + admin_api=True, + extra_id="dom0-dispvm-preload-gui-api", + from_dom0=True, + ), + TestConfig( + "dom0-dispvm-preload-concurrent-api", + concurrent=True, + preload_max=MAX_CONCURRENCY, admin_api=True, from_dom0=True, ), @@ -557,7 +604,8 @@ def call_api(self, test, service, qube): else: appvm = domains[qube] domain_time = get_time() - target_qube = qubesadmin.vm.DispVM.from_appvm(app, appvm) + target_wrapper = qubesadmin.vm.DispVM.from_appvm(app, appvm) + target_qube = target_wrapper.create_disposable() name = target_qube.name # A very small number, if it appears, it will show a bottleneck at # DispVM.from_appvm. diff --git a/tests/dispvm_perf_reader.py b/tests/dispvm_perf_reader.py index 2c1d12674..877808727 100755 --- a/tests/dispvm_perf_reader.py +++ b/tests/dispvm_perf_reader.py @@ -235,12 +235,23 @@ def __init__( ) if not self.data: logging.critical( - "Default template specified not found: '%s'", + "Template specified not found: '%s'", self.default_template, ) sys.exit(1) else: logging.info("Default template not specified, using newest fedora") + fedora_templates = [ + v + for v in self.orig_data.values() + if v["os-distribution"] == "fedora" + ] + if not fedora_templates: + logging.critical( + "Tried to find any fedora template but ultimately failed", + self.default_template, + ) + sys.exit(1) fedora_template = max( ( v @@ -818,72 +829,126 @@ def graph_03_stage_dist(self) -> None: def graph_04_line(self) -> None: """Line graph of performance of each iteration.""" - def filter_preload(value, cond): # pylint: disable=unused-argument - return value == 0 or (3 <= value <= 5) + def filter_preload_small(value, _cond): + return value <= 3 - tests = filter_data( + def filter_preload(value, _cond): + return value == 0 or (4 <= value <= 6) + + base_tests = filter_data( self.api_tests, { "non_dispvm": False, "admin_api": True, "concurrent": False, - "gui": False, - "preload_max": None, - }, - { - "preload_max": filter_preload, }, ) + base_tests_gui = filter_data(base_tests, {"gui": True}) + base_tests_nogui = filter_data(base_tests, {"gui": False}) + + tests = filter_data( + base_tests_nogui, + {"preload_max": None}, + {"preload_max": filter_preload}, + ) + tests_small = filter_data( + base_tests_nogui, + {"preload_max": None}, + {"preload_max": filter_preload_small}, + ) + + tests_gui = filter_data( + base_tests_gui, + {"preload_max": None}, + {"preload_max": filter_preload}, + ) + tests_small_gui = filter_data( + base_tests_gui, + {"preload_max": None}, + {"preload_max": filter_preload_small}, + ) + tests = dict(sorted(tests.items(), key=lambda x: x[1]["preload_max"])) - for num, stage in enumerate(["exec", "clean", "total"]): - fig, axs = plt.subplots(figsize=(WIDTH * 3, HEIGHT * 3)) - points_seen = set() - for test in tests.values(): - name = test["pretty_name"] - iteration_data = test.get("api_results", {}).get( - "iteration", {} + tests_small = dict( + sorted(tests_small.items(), key=lambda x: x[1]["preload_max"]) + ) + tests_gui = dict( + sorted(tests_gui.items(), key=lambda x: x[1]["preload_max"]) + ) + tests_small_gui = dict( + sorted(tests_small_gui.items(), key=lambda x: x[1]["preload_max"]) + ) + all_tests = { + "nogui-small": { + "for simple calls with some preloaded disposables": tests_small + }, + "gui-small": { + "for GUI calls with some preloaded disposables": tests_small_gui + }, + "nogui-large": { + "for simple calls with some preloaded disposables": tests + }, + "gui-large": { + "for GUI calls with some preloaded disposables": tests_gui + }, + } + + for test_type, test_value in all_tests.items(): + desc = list(test_value.keys())[0] + tests = list(test_value.values())[0] + for num, stage in enumerate(["exec", "clean", "total"]): + fig, axs = plt.subplots(figsize=(WIDTH * 3, HEIGHT * 3)) + points_seen = set() + for test in tests.values(): + name = test["pretty_name"] + iteration_data = test.get("api_results", {}).get( + "iteration", {} + ) + iterations = range(1, test["iterations"] + 1) + stage_data = test.get("api_results", {}).get("stage", {}) + mean = round(stage_data[stage]["mean"], 1) + times = [ + iteration_data[str(it)][stage] for it in iterations + ] + axs.plot( + iterations, + times, + label=f"{name} \u03bc {mean}", + linestyle="--", + linewidth=3, + ) + rounder = 1 + for x_val, y_val in zip(iterations, times): + if (x_val, round(y_val, rounder)) in points_seen: + continue + points_seen.add((x_val, round(y_val, rounder))) + axs.text( + x_val, + y_val, + str(round(y_val, rounder)), + ha="center", + fontsize="xx-large", + ) + if stage == "total": + stage_pretty = stage + else: + stage_pretty = self.stage_dict[stage]["legend"].lower() + axs.set_ylabel("Time (seconds)") + axs.set_title( + f"{stage_pretty.capitalize()} time per iteration {desc}" ) - iterations = range(1, test["iterations"] + 1) - stage_data = test.get("api_results", {}).get("stage", {}) - mean = round(stage_data[stage]["mean"], 1) - times = [iteration_data[str(it)][stage] for it in iterations] - axs.plot( - iterations, - times, - label=f"{name} \u03bc {mean}", - linestyle="--", - linewidth=3, + axs.set_xticks(iterations) + axs.legend() + caption = ( + f"Compares the {stage_pretty} per iteration of normal " + + "disposables with preloaded disposables." ) - rounder = 1 - for x_val, y_val in zip(iterations, times): - if (x_val, round(y_val, rounder)) in points_seen: - continue - points_seen.add((x_val, round(y_val, rounder))) - axs.text( - x_val, - y_val, - str(round(y_val, rounder)), - ha="center", - fontsize="xx-large", - ) - if stage == "total": - stage_pretty = stage - else: - stage_pretty = self.stage_dict[stage]["legend"].lower() - axs.set_ylabel("Time (seconds)") - axs.set_title(f"{stage_pretty.capitalize()} time per iteration") - axs.set_xticks(iterations) - axs.legend() - caption = ( - f"Compares the {stage_pretty} per iteration of normal " - + "disposables with preloaded disposables." - ) - fig.supxlabel( - caption, - wrap=True, - color=CAPTION_COLOR, - ) - self.end(plt, str(num) + "_" + stage) + fig.supxlabel( + caption, + wrap=True, + color=CAPTION_COLOR, + ) + self.end(plt, str(num) + "_" + stage + "_" + test_type) def graph_08_template(self) -> None: """Graph including all available templates.""" @@ -923,9 +988,18 @@ def graph_09_method(self) -> None: orig_tests = filter_data( self.data, {"non_dispvm": False, "extra_id": ""} ) + + concurrent_tests = filter_data(orig_tests, {"concurrent": True}) + gui_tests = filter_data(orig_tests, {"gui": True}) + # TODO: ben: some issue here for query in ["concurrent-gui", "gui", "concurrent", "gui-concurrent"]: + if ("concurrent" in query and not concurrent_tests) or ( + "gui" in query and not gui_tests + ): + logging.warning("Skipped query without tests: %s", query) + continue if query == "concurrent-gui": - tests = filter_data(orig_tests, {"concurrent": True}) + tests = concurrent_tests query_name = "with concurrency (with and without GUI)," query_file = "wconc_nogui_gui" elif query == "gui": @@ -937,7 +1011,7 @@ def graph_09_method(self) -> None: query_name = "with and without concurrency," query_file = "noconc_conc" elif query == "gui-concurrent": - tests = filter_data(orig_tests, {"gui": True}) + tests = gui_tests query_name = "with GUI (with and without concurrency)," query_file = "wgui_noconc_conc" else: @@ -952,7 +1026,7 @@ def graph_09_method(self) -> None: vm_qrexec = filter_data( tests, {"from_dom0": False, "admin_api": False} ) - + method_tests = [t for t in [dom0_api, dom0_qvm, vm_qrexec] if t] for key in ["mean", "total"]: preload_tests = filter_data( tests, {"preload_max": 1}, {"preload_max": operator.ge} @@ -960,11 +1034,11 @@ def graph_09_method(self) -> None: assert_items_match(preload_tests, ["preload_max"]) preload_max = get_value(preload_tests, "preload_max")[0] caption = ( - f"Compares workflows of normal disposables with " + "Compares workflows of normal disposables with " + f"{preload_max} preloaded disposables." ) self.bar_plot( - [dom0_api, dom0_qvm, vm_qrexec], + method_tests, titles=["dom0 API", "dom0 qvm", "qube qrexec-client-vm"], title_prefix=False, keys=[key], From 64088471407b32bef923629ebbd22957a989cafb Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Fri, 12 Dec 2025 02:08:41 +0100 Subject: [PATCH 6/8] Time startup of normal disposable --- tests/dispvm_perf.py | 24 +++- tests/dispvm_perf_reader.py | 239 +++++++++++++++++++++++++++--------- 2 files changed, 200 insertions(+), 63 deletions(-) diff --git a/tests/dispvm_perf.py b/tests/dispvm_perf.py index 6ece0f21d..28d1b603f 100755 --- a/tests/dispvm_perf.py +++ b/tests/dispvm_perf.py @@ -596,29 +596,37 @@ def call_api(self, test, service, qube): start_time = get_time() app = qubesadmin.Qubes() domains = app.domains + target_time = None + startup_time = None if test.non_dispvm: # Even though we already have the qube object passed from the # class, assume we don't so we can calculate gathering. target_qube = domains[self.vm1.name] domain_time = get_time() + pre_exec_time = domain_time else: appvm = domains[qube] domain_time = get_time() target_wrapper = qubesadmin.vm.DispVM.from_appvm(app, appvm) target_qube = target_wrapper.create_disposable() - name = target_qube.name - # A very small number, if it appears, it will show a bottleneck at - # DispVM.from_appvm. - target_time = get_time() + target_time = get_time() + if not test.preload_max: + target_qube.start() + startup_time = get_time() + pre_exec_time = startup_time + else: + pre_exec_time = target_time try: target_qube.run_service_for_stdio(service, timeout=60) except subprocess.CalledProcessError as e: + name = target_qube.name raise Exception( f"'{name}': service '{service}' failed ({e.returncode}):" f" {e.stdout}," f" {e.stderr}" ) except subprocess.TimeoutExpired as e: + name = target_qube.name raise Exception( f"'{name}': service '{service}' failed: timeout expired:" f" {e.stdout}," @@ -635,7 +643,13 @@ def call_api(self, test, service, qube): runtime["dom"] = round(domain_time - start_time, ROUND_PRECISION) if not test.non_dispvm: runtime["disp"] = round(target_time - domain_time, ROUND_PRECISION) - runtime["exec"] = round(run_service_time - target_time, ROUND_PRECISION) + if not test.preload_max: + runtime["start"] = round( + startup_time - target_time, ROUND_PRECISION + ) + runtime["exec"] = round( + run_service_time - pre_exec_time, ROUND_PRECISION + ) if not test.non_dispvm: runtime["clean"] = round( cleanup_time - run_service_time, ROUND_PRECISION diff --git a/tests/dispvm_perf_reader.py b/tests/dispvm_perf_reader.py index 877808727..c3db859af 100755 --- a/tests/dispvm_perf_reader.py +++ b/tests/dispvm_perf_reader.py @@ -290,7 +290,7 @@ def __init__( self.dispvm_api_tests = filter_data( self.api_tests, { - "non_dispvm": False, + "target_dispvm": True, "admin_api": True, "concurrent": False, "extra_id": "", @@ -301,7 +301,7 @@ def __init__( self.dispvm_tests = filter_data( self.data, { - "non_dispvm": False, + "target_dispvm": True, "admin_api": False, "concurrent": False, "extra_id": "", @@ -322,15 +322,30 @@ def __init__( for name, test in self.dispvm_tests.items() ] + self.vm_stage_dict = { + "dom": { + "color": COLORS["Icon Dark Gray"], + "legend": "Initialization", + }, + "exec": {"color": COLORS["Warning Orange"], "legend": "Execution"}, + } + self.stage_dict = { - "dom": {"color": COLORS["Icon Dark Gray"], "legend": "Others"}, - "disp": {"color": COLORS["Middle Gray"], "legend": "Others"}, + "dom": { + "color": COLORS["Icon Dark Gray"], + "legend": "Initialization", + }, + "disp": {"color": COLORS["Purple"], "legend": "Creation"}, + "start": {"color": COLORS["Danger Red"], "legend": "Startup"}, "exec": {"color": COLORS["Warning Orange"], "legend": "Execution"}, "clean": {"color": COLORS["Sub Gray"], "legend": "Cleanup"}, } self.preload_stage_dict = { - "dom": {"color": COLORS["Icon Dark Gray"], "legend": "Others"}, - "disp": {"color": COLORS["Middle Gray"], "legend": "Others"}, + "dom": { + "color": COLORS["Icon Dark Gray"], + "legend": "Initialization", + }, + "disp": {"color": COLORS["Purple"], "legend": "Creation"}, "exec": {"color": COLORS["Primary Blue"], "legend": "Execution"}, "clean": {"color": COLORS["Sub Gray"], "legend": "Cleanup"}, } @@ -339,7 +354,7 @@ def __init__( for test, result in self.api_tests.items(): if result.get("api_results", {}).get("stage", {}): self.stages.extend(result["api_results"]["stage"].keys()) - self.stage_order = ["dom", "disp", "exec", "clean", "total"] + self.stage_order = [*self.stage_dict.keys(), "total"] self.stages = sorted( list(set(self.stages)), key=lambda s: ( @@ -354,7 +369,6 @@ def run(self) -> None: """Loop through all enabled graphs.""" avail_graphs = get_graphs() failed_graphs = [] - # TODO: almost every graph should target all templates for graph in avail_graphs: method = getattr(self, "graph_" + graph.replace("-", "_")) if not (not self.graphs or graph in self.graphs): @@ -415,7 +429,7 @@ def bar_plot( supxlabel: str = "", file_prefix: str = "", ) -> None: - # pylint: disable=too-many-locals + # pylint: disable=too-many-locals,too-many-positional-arguments """ Assemble figure of bar plot where normal and preload tests are grouped. Each test passed is subplotted. @@ -647,6 +661,7 @@ def graph_01_bar(self) -> None: def graph_02_stage_stack(self) -> None: """Stacked bar by stage graph.""" + # pylint: disable=too-many-locals tests = filter_data(self.dispvm_api_tests, {"preload_max": 0}) assert_items_match(tests, ["iterations"]) iterations = get_value(tests, "iterations")[0] @@ -657,15 +672,23 @@ def graph_02_stage_stack(self) -> None: {"preload_max": operator.ge}, ) - important_stages = ["exec", "clean", "total"] label_array = np.arange(len(tests_names_pretty)) label_count = len(tests_names_pretty) width = 0.8 / label_count + # Don't count "dom" stage, when there are other large values, it + # overlaps the label. up_to_exec = self.stages[:-2] - for num, stage_intro in enumerate([up_to_exec, self.stages]): + up_to_exec.remove("dom") + up_to_last = self.stages + up_to_last.remove("dom") + important_stages = up_to_last + # pylint: disable=too-many-nested-blocks + for num, stage_intro in enumerate([up_to_exec, up_to_last]): fig, axs = plt.subplots(figsize=(WIDTH * 3, HEIGHT * 3)) bottom = np.zeros(len(tests_names_pretty)) bottom_preload = np.zeros(len(tests_names_pretty)) + stored_means_stage = [] + stored_preload_means_stage = [] for stage in stage_intro: means_stage = np.array( [ @@ -673,18 +696,27 @@ def graph_02_stage_stack(self) -> None: for test in tests.values() ] ) - preload_means_stage = np.array( - [ - test["api_results"]["stage"][stage]["mean"] - for test in preload_tests.values() - ] - ) + stored_means_stage.append(means_stage) + if stage == "start": + preload_means_stage = np.zeros(len(means_stage)) + else: + preload_means_stage = np.array( + [ + test["api_results"]["stage"][stage]["mean"] + for test in preload_tests.values() + ] + ) + stored_preload_means_stage.append(preload_means_stage) assert_items_length_match(tests, preload_tests) if stage in ["exec", "total"]: + sum_means_stage = [sum(x) for x in zip(*stored_means_stage)] + sum_preload_means_stage = [ + sum(x) for x in zip(*stored_preload_means_stage) + ] ratio = np.divide( - means_stage, - preload_means_stage, + sum_means_stage, + sum_preload_means_stage, out=np.zeros_like(means_stage, dtype=float), where=preload_means_stage != 0, ) @@ -695,12 +727,21 @@ def graph_02_stage_stack(self) -> None: if stage == "exec" and "total" in stage_intro: break xpos = label_array[i] - min_mean = min(means_stage[i], preload_means_stage[i]) - max_mean = max(means_stage[i], preload_means_stage[i]) - height = max( - min_mean + 1, min_mean + ((max_mean - min_mean) / 2) - ) - height = min_mean + 1 + if stage == "total": + min_mean = min( + means_stage[i], preload_means_stage[i] + ) + max_mean = max( + means_stage[i], preload_means_stage[i] + ) + height = max( + min_mean + 1, + min_mean + ((max_mean - min_mean) / 2), + ) + else: + height = ( + sum_preload_means_stage[i] + sum_means_stage[i] + ) / 2 if ratio[i] > 1: color = COLORS["Success Green"] else: @@ -726,19 +767,21 @@ def graph_02_stage_stack(self) -> None: bottom += means_stage positions = label_array + width / 2 - bars_preload = axs.bar( - positions, - preload_means_stage, - width=width, - bottom=bottom_preload, - color=self.preload_stage_dict[stage]["color"], - ) + if stage != "start": + bars_preload = axs.bar( + positions, + preload_means_stage, + width=width, + bottom=bottom_preload, + color=self.preload_stage_dict[stage]["color"], + ) bottom_preload += preload_means_stage if ( stage in important_stages - or stage == list(self.stage_dict.keys())[-1] + or stage == list(self.stages)[-1] ): + # pylint: disable=possibly-used-before-assignment for bbar in [bars_normal, bars_preload]: axs.bar_label( bbar, @@ -746,7 +789,26 @@ def graph_02_stage_stack(self) -> None: label_type="center", fontsize="xx-large", ) - + if stage not in ["exec", "clean"]: + continue + if stage == "exec" and "total" in stage_intro: + continue + axs.bar_label(bbar, fmt="%.2f", fontsize="xx-large") + + means_stage_total = [ + test["api_results"]["stage"]["total"]["mean"] + for test in tests.values() + ] + preload_means_stage_total = [ + test["api_results"]["stage"]["total"]["mean"] + for test in preload_tests.values() + ] + top_value = math.ceil( + max(*means_stage_total, *preload_means_stage_total) + ) + top_tick = int(10 * top_value / 10) + yticks = [round(x, 1) for x in np.linspace(0, top_tick, 4)] + plt.yticks(yticks) plt.xticks(label_array, tests_names_pretty) plt.ylabel("Time (s)") pretty_iterations = " over {} iterations".format(iterations) @@ -759,7 +821,7 @@ def graph_02_stage_stack(self) -> None: if stage in stage_intro ] legend_handles.insert( - -2, + -3, *[ matplotlib.patches.Patch( color=value["color"], label="Preload " + value["legend"] @@ -782,20 +844,41 @@ def graph_02_stage_stack(self) -> None: wrap=True, color=CAPTION_COLOR, ) - self.end(plt, str(num)) + self.end(plt, str(num) + "_until_" + stage_intro[-1]) def graph_03_stage_dist(self) -> None: """Scattered and jittered stage distribution graph.""" - tests = self.dispvm_api_tests + tests = filter_data( + self.api_tests, + { + "concurrent": False, + "extra_id": "", + }, + ) assert_items_match(tests, ["iterations"]) tests_names_pretty = get_value(tests, "pretty_name") - for num, stage in enumerate(["exec", "clean", "total"]): + for num, stage in enumerate(self.stages + ["response"]): stage_values = [] for _, test in tests.items(): - stage_values.append( - test["api_results"]["stage"][stage]["values"] - ) - fig, axs = plt.subplots(figsize=(WIDTH * 3, HEIGHT * 3)) + api_results = test["api_results"]["stage"] + if stage == "response": + if test["target_dispvm"]: + stage_values.append( + [ + x - y + for x, y in zip( + api_results["total"]["values"], + api_results["clean"]["values"], + ) + ] + ) + else: + stage_values.append(api_results["total"]["values"]) + else: + stage_values.append( + api_results.get(stage, {}).get("values", []) + ) + fig, axs = plt.subplots(figsize=(WIDTH * 4, HEIGHT * 4)) x_pos = np.arange(1, len(stage_values) + 1) jitter_strength = 0.1 for i, values in enumerate(stage_values, start=1): @@ -810,7 +893,7 @@ def graph_03_stage_dist(self) -> None: axs.scatter(x_jittered, values, s=100, alpha=0.8, color=color) axs.set_xticks(x_pos, wrap_text(tests_names_pretty, 25)) axs.set_ylabel("Time (s)") - if stage == "total": + if stage in ["total", "response"]: stage_pretty = stage else: stage_pretty = self.stage_dict[stage]["legend"].lower() @@ -838,7 +921,6 @@ def filter_preload(value, _cond): base_tests = filter_data( self.api_tests, { - "non_dispvm": False, "admin_api": True, "concurrent": False, }, @@ -880,10 +962,10 @@ def filter_preload(value, _cond): ) all_tests = { "nogui-small": { - "for simple calls with some preloaded disposables": tests_small + "for simple calls with few preloaded disposables": tests_small }, "gui-small": { - "for GUI calls with some preloaded disposables": tests_small_gui + "for GUI calls with few preloaded disposables": tests_small_gui }, "nogui-large": { "for simple calls with some preloaded disposables": tests @@ -893,23 +975,57 @@ def filter_preload(value, _cond): }, } - for test_type, test_value in all_tests.items(): + num_list = [] + rounder = 2 + # pylint: disable=too-many-nested-blocks + for idx, (test_type, test_value) in enumerate(all_tests.items()): desc = list(test_value.keys())[0] tests = list(test_value.values())[0] - for num, stage in enumerate(["exec", "clean", "total"]): + for num, stage in enumerate(self.stages + ["response"]): + num_list.append(num) fig, axs = plt.subplots(figsize=(WIDTH * 3, HEIGHT * 3)) points_seen = set() for test in tests.values(): + if ( + not test["target_dispvm"] + and stage != "response" + and stage not in self.vm_stage_dict + ): + continue name = test["pretty_name"] iteration_data = test.get("api_results", {}).get( "iteration", {} ) iterations = range(1, test["iterations"] + 1) stage_data = test.get("api_results", {}).get("stage", {}) - mean = round(stage_data[stage]["mean"], 1) - times = [ - iteration_data[str(it)][stage] for it in iterations - ] + if not (stage == "start" and test["preload_max"] > 0): + if stage == "response": + if test["target_dispvm"]: + mean_response = ( + stage_data["total"]["mean"] + - stage_data["clean"]["mean"] + ) + mean = round(mean_response, rounder) + times = [ + iteration_data[str(it)]["total"] + - iteration_data[str(it)]["clean"] + for it in iterations + ] + else: + mean_response = stage_data["total"]["mean"] + mean = round(mean_response, rounder) + times = [ + iteration_data[str(it)]["total"] + for it in iterations + ] + else: + mean = round(stage_data[stage]["mean"], rounder) + times = [ + iteration_data[str(it)][stage] + for it in iterations + ] + else: + continue axs.plot( iterations, times, @@ -917,7 +1033,6 @@ def filter_preload(value, _cond): linestyle="--", linewidth=3, ) - rounder = 1 for x_val, y_val in zip(iterations, times): if (x_val, round(y_val, rounder)) in points_seen: continue @@ -929,7 +1044,7 @@ def filter_preload(value, _cond): ha="center", fontsize="xx-large", ) - if stage == "total": + if stage in ["total", "response"]: stage_pretty = stage else: stage_pretty = self.stage_dict[stage]["legend"].lower() @@ -948,7 +1063,15 @@ def filter_preload(value, _cond): wrap=True, color=CAPTION_COLOR, ) - self.end(plt, str(num) + "_" + stage + "_" + test_type) + idx_str = str(idx) + if idx < 10: + idx_str = "0" + idx_str + self.end( + plt, + "{}_{}_{}_{}".format( + str(idx_str), str(num), stage, test_type + ), + ) def graph_08_template(self) -> None: """Graph including all available templates.""" @@ -956,7 +1079,7 @@ def graph_08_template(self) -> None: self.orig_data, { "admin_api": True, - "non_dispvm": False, + "target_dispvm": True, "extra_id": "", "concurrent": False, }, @@ -986,12 +1109,11 @@ def graph_08_template(self) -> None: def graph_09_method(self) -> None: """Graph including different callers comparing GUI and concurrency.""" orig_tests = filter_data( - self.data, {"non_dispvm": False, "extra_id": ""} + self.data, {"target_dispvm": True, "extra_id": ""} ) concurrent_tests = filter_data(orig_tests, {"concurrent": True}) gui_tests = filter_data(orig_tests, {"gui": True}) - # TODO: ben: some issue here for query in ["concurrent-gui", "gui", "concurrent", "gui-concurrent"]: if ("concurrent" in query and not concurrent_tests) or ( "gui" in query and not gui_tests @@ -1142,6 +1264,7 @@ def main() -> None: logging.info("Loaded data") if args.output_dir: os.makedirs(args.output_dir, exist_ok=True) + logging.info("Output directory is %s", repr(args.output_dir)) graph = Graph( data, default_template=args.template, From 6cd723ee4cf87fa8730ec4c61caad47894b52a05 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Fri, 12 Dec 2025 02:35:51 +0100 Subject: [PATCH 7/8] Use affirmative variable for disposable tests For: https://github.com/QubesOS/qubes-issues/issues/1512 --- tests/dispvm_perf.py | 68 +++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/tests/dispvm_perf.py b/tests/dispvm_perf.py index 28d1b603f..f65ec20c6 100755 --- a/tests/dispvm_perf.py +++ b/tests/dispvm_perf.py @@ -91,7 +91,7 @@ class TestConfig: :param bool concurrent: request concurrently :param bool from_dom0: initiate call from dom0 :param int preload_max: number of disposables to preload - :param bool non_dispvm: target a non disposable qube + :param bool target_dispvm: target or not a disposable qube :param bool admin_api: use the Admin API directly :param str extra_id: base test that extra ID varies from :param str pretty_name: human-readable name @@ -143,7 +143,7 @@ class TestConfig: concurrent: bool = False from_dom0: bool = False preload_max: int = 0 - non_dispvm: bool = False + target_dispvm: bool = True admin_api: bool = False extra_id: str = "" pretty_name: str = dataclasses.field(init=False) @@ -171,9 +171,7 @@ def __post_init__(self): else: pretty_what = "simple" - if self.non_dispvm: - pretty_to = "in another running qube" - else: + if self.target_dispvm: disp_suffix = "" disp_prefix = "a " if self.preload_max: @@ -189,6 +187,8 @@ def __post_init__(self): disp_prefix = "" disp_suffix = "s" pretty_to = "in {}disposable{}".format(disp_prefix, disp_suffix) + else: + pretty_to = "in another running qube" pretty_name = "{} runs".format(pretty_from.capitalize()) if pretty_strategy: @@ -223,24 +223,26 @@ def __post_init__(self): ROUND_PRECISION = 3 ALL_TESTS = [ - TestConfig("vm-vm", non_dispvm=True), - TestConfig("vm-vm-gui", gui=True, non_dispvm=True), - TestConfig("vm-vm-concurrent", concurrent=True, non_dispvm=True), + TestConfig("vm-vm", target_dispvm=False), + TestConfig("vm-vm-gui", gui=True, target_dispvm=False), + TestConfig("vm-vm-concurrent", concurrent=True, target_dispvm=False), TestConfig( - "vm-vm-gui-concurrent", gui=True, concurrent=True, non_dispvm=True + "vm-vm-gui-concurrent", gui=True, concurrent=True, target_dispvm=False + ), + TestConfig( + "dom0-vm-api", target_dispvm=False, admin_api=True, from_dom0=True ), - TestConfig("dom0-vm-api", non_dispvm=True, admin_api=True, from_dom0=True), TestConfig( "dom0-vm-gui-api", gui=True, - non_dispvm=True, + target_dispvm=False, admin_api=True, from_dom0=True, ), TestConfig( "dom0-vm-concurrent-api", concurrent=True, - non_dispvm=True, + target_dispvm=False, admin_api=True, from_dom0=True, ), @@ -248,7 +250,7 @@ def __post_init__(self): "dom0-vm-gui-concurrent-api", gui=True, concurrent=True, - non_dispvm=True, + target_dispvm=False, admin_api=True, from_dom0=True, ), @@ -542,10 +544,10 @@ def run_latency_calls(self, test): caller += f"--dispvm={self.dvm.name} " cmd = f"{caller} -- {service}" else: - if test.non_dispvm: - target = self.vm2.name - else: + if test.target_dispvm: target = "@dispvm" + else: + target = self.vm2.name cmd = f"qrexec-client-vm -- {target} {service}" code = ( @@ -598,13 +600,7 @@ def call_api(self, test, service, qube): domains = app.domains target_time = None startup_time = None - if test.non_dispvm: - # Even though we already have the qube object passed from the - # class, assume we don't so we can calculate gathering. - target_qube = domains[self.vm1.name] - domain_time = get_time() - pre_exec_time = domain_time - else: + if test.target_dispvm: appvm = domains[qube] domain_time = get_time() target_wrapper = qubesadmin.vm.DispVM.from_appvm(app, appvm) @@ -616,6 +612,12 @@ def call_api(self, test, service, qube): pre_exec_time = startup_time else: pre_exec_time = target_time + else: + # Even though we already have the qube object passed from the + # class, assume we don't so we can calculate gathering. + target_qube = domains[self.vm1.name] + domain_time = get_time() + pre_exec_time = domain_time try: target_qube.run_service_for_stdio(service, timeout=60) except subprocess.CalledProcessError as e: @@ -633,7 +635,7 @@ def call_api(self, test, service, qube): f" {e.stderr}" ) run_service_time = get_time() - if not test.non_dispvm: + if test.target_dispvm: target_qube.cleanup() cleanup_time = get_time() end_time = cleanup_time @@ -641,7 +643,7 @@ def call_api(self, test, service, qube): end_time = get_time() runtime = {} runtime["dom"] = round(domain_time - start_time, ROUND_PRECISION) - if not test.non_dispvm: + if test.target_dispvm: runtime["disp"] = round(target_time - domain_time, ROUND_PRECISION) if not test.preload_max: runtime["start"] = round( @@ -650,7 +652,7 @@ def call_api(self, test, service, qube): runtime["exec"] = round( run_service_time - pre_exec_time, ROUND_PRECISION ) - if not test.non_dispvm: + if test.target_dispvm: runtime["clean"] = round( cleanup_time - run_service_time, ROUND_PRECISION ) @@ -675,10 +677,10 @@ async def run_latency_api_calls(self, test): service = self.gui_service else: service = self.nogui_service - if test.non_dispvm: - qube = self.vm2 - else: + if test.target_dispvm: qube = self.dvm + else: + qube = self.vm2 results = {} results["api_results"] = {} @@ -835,10 +837,10 @@ async def run_test(self, test: TestConfig): with open(POLICY_FILE, "w", encoding="ascii") as policy: gui_prefix = f"{self.gui_service} * {self.vm1.name}" nogui_prefix = f"{self.nogui_service} * {self.vm1.name}" - if test.non_dispvm: - target = f"{self.vm2.name}" - else: + if test.target_dispvm: target = "@dispvm" + else: + target = f"{self.vm2.name}" policy.write( f"{gui_prefix} {target} allow\n" f"{nogui_prefix} {target} allow\n" @@ -912,7 +914,7 @@ async def run_test(self, test: TestConfig): if not os.getenv("QUBES_TEST_SKIP_TEARDOWN_SLEEP"): logger.info("Load before sleep: '%s'", get_load()) delay = 5 - if not test.non_dispvm: + if test.target_dispvm: delay += 10 if test.gui: delay += 2 From a3c02a3d1f364b22e0b6aad0ac01742bc60d7515 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Fri, 12 Dec 2025 18:58:04 +0100 Subject: [PATCH 8/8] Add disposable benchmark from OpenQA --- tests-data/dispvm_perf/2025121204-4.3.json | 1638 ++++++++++++++++++++ 1 file changed, 1638 insertions(+) create mode 100644 tests-data/dispvm_perf/2025121204-4.3.json diff --git a/tests-data/dispvm_perf/2025121204-4.3.json b/tests-data/dispvm_perf/2025121204-4.3.json new file mode 100644 index 000000000..0fb6568a9 --- /dev/null +++ b/tests-data/dispvm_perf/2025121204-4.3.json @@ -0,0 +1,1638 @@ +{ + "debian-13-xfce_dom0-dispvm-api": { + "name": "dom0-dispvm-api", + "gui": false, + "concurrent": false, + "from_dom0": true, + "preload_max": 0, + "target_dispvm": true, + "admin_api": true, + "extra_id": "", + "pretty_name": "Dom0 runs simple app in a disposable (API)", + "template": "debian-13-xfce", + "api_results": { + "iteration": { + "1": { + "dom": 0.016, + "disp": 0.68, + "start": 5.745, + "exec": 0.318, + "clean": 1.204, + "total": 7.963 + }, + "2": { + "dom": 0.02, + "disp": 0.645, + "start": 6.093, + "exec": 0.274, + "clean": 0.958, + "total": 7.989 + }, + "3": { + "dom": 0.013, + "disp": 0.863, + "start": 5.836, + "exec": 0.349, + "clean": 0.961, + "total": 8.022 + }, + "4": { + "dom": 0.02, + "disp": 0.58, + "start": 4.967, + "exec": 0.378, + "clean": 0.934, + "total": 6.879 + }, + "5": { + "dom": 0.019, + "disp": 0.764, + "start": 4.981, + "exec": 0.475, + "clean": 1.207, + "total": 7.446 + }, + "6": { + "dom": 0.016, + "disp": 0.463, + "start": 5.329, + "exec": 0.312, + "clean": 0.956, + "total": 7.077 + }, + "7": { + "dom": 0.015, + "disp": 0.523, + "start": 6.468, + "exec": 0.266, + "clean": 1.067, + "total": 8.338 + }, + "8": { + "dom": 0.019, + "disp": 0.485, + "start": 4.68, + "exec": 0.455, + "clean": 0.856, + "total": 6.496 + }, + "9": { + "dom": 0.016, + "disp": 0.438, + "start": 5.182, + "exec": 0.319, + "clean": 1.096, + "total": 7.051 + }, + "10": { + "dom": 0.014, + "disp": 0.644, + "start": 5.536, + "exec": 0.309, + "clean": 0.952, + "total": 7.454 + }, + "11": { + "dom": 0.018, + "disp": 0.489, + "start": 5.324, + "exec": 0.487, + "clean": 0.99, + "total": 7.309 + }, + "12": { + "dom": 0.025, + "disp": 0.622, + "start": 5.592, + "exec": 0.301, + "clean": 0.916, + "total": 7.457 + } + }, + "stage": { + "dom": { + "values": [ + 0.016, + 0.02, + 0.013, + 0.02, + 0.019, + 0.016, + 0.015, + 0.019, + 0.016, + 0.014, + 0.018, + 0.025 + ], + "mean": 0.018, + "median": 0.017 + }, + "disp": { + "values": [ + 0.68, + 0.645, + 0.863, + 0.58, + 0.764, + 0.463, + 0.523, + 0.485, + 0.438, + 0.644, + 0.489, + 0.622 + ], + "mean": 0.6, + "median": 0.601 + }, + "start": { + "values": [ + 5.745, + 6.093, + 5.836, + 4.967, + 4.981, + 5.329, + 6.468, + 4.68, + 5.182, + 5.536, + 5.324, + 5.592 + ], + "mean": 5.478, + "median": 5.432 + }, + "exec": { + "values": [ + 0.318, + 0.274, + 0.349, + 0.378, + 0.475, + 0.312, + 0.266, + 0.455, + 0.319, + 0.309, + 0.487, + 0.301 + ], + "mean": 0.354, + "median": 0.319 + }, + "clean": { + "values": [ + 1.204, + 0.958, + 0.961, + 0.934, + 1.207, + 0.956, + 1.067, + 0.856, + 1.096, + 0.952, + 0.99, + 0.916 + ], + "mean": 1.008, + "median": 0.96 + }, + "total": { + "values": [ + 7.963, + 7.989, + 8.022, + 6.879, + 7.446, + 7.077, + 8.338, + 6.496, + 7.051, + 7.454, + 7.309, + 7.457 + ], + "mean": 7.457, + "median": 7.45 + } + } + }, + "iterations": 12, + "mean": 7.457, + "total": 89.481, + "date": "2025-12-12T12:00:33", + "memory": 400, + "maxmem": 4000, + "vcpus": 2, + "qrexec_timeout": 60, + "shutdown_timeout": 60, + "kernel": "6.17.9-1.fc41", + "kernelopts": "swiotlb=2048", + "template-buildtime": "2025-09-19 11:46:06", + "last-update": "2025-09-22 11:32:55", + "os": "Linux", + "os-distribution": "debian", + "os-version": 13, + "hcl-qubes": "R4.3-rc3", + "hcl-xen": "4.19.3", + "hcl-kernel": "6.17.9-1", + "hcl-memory": 32404 + }, + "debian-13-xfce_dom0-dispvm-gui-api": { + "name": "dom0-dispvm-gui-api", + "gui": true, + "concurrent": false, + "from_dom0": true, + "preload_max": 0, + "target_dispvm": true, + "admin_api": true, + "extra_id": "", + "pretty_name": "Dom0 runs GUI app in a disposable (API)", + "template": "debian-13-xfce", + "api_results": { + "iteration": { + "1": { + "dom": 0.016, + "disp": 0.581, + "start": 4.878, + "exec": 2.447, + "clean": 1.021, + "total": 8.942 + }, + "2": { + "dom": 0.014, + "disp": 0.675, + "start": 5.334, + "exec": 2.023, + "clean": 1.08, + "total": 9.126 + }, + "3": { + "dom": 0.017, + "disp": 0.578, + "start": 5.065, + "exec": 1.629, + "clean": 1.077, + "total": 8.365 + }, + "4": { + "dom": 0.015, + "disp": 0.467, + "start": 4.796, + "exec": 1.649, + "clean": 1.182, + "total": 8.109 + }, + "5": { + "dom": 0.02, + "disp": 0.627, + "start": 5.964, + "exec": 1.734, + "clean": 1.0, + "total": 9.346 + }, + "6": { + "dom": 0.017, + "disp": 0.516, + "start": 5.865, + "exec": 1.562, + "clean": 1.132, + "total": 9.092 + }, + "7": { + "dom": 0.015, + "disp": 0.462, + "start": 5.304, + "exec": 1.927, + "clean": 0.999, + "total": 8.706 + }, + "8": { + "dom": 0.018, + "disp": 0.608, + "start": 5.74, + "exec": 1.616, + "clean": 0.947, + "total": 8.93 + }, + "9": { + "dom": 0.014, + "disp": 0.518, + "start": 6.002, + "exec": 1.743, + "clean": 1.028, + "total": 9.306 + }, + "10": { + "dom": 0.021, + "disp": 0.839, + "start": 5.442, + "exec": 1.851, + "clean": 1.009, + "total": 9.162 + }, + "11": { + "dom": 0.013, + "disp": 0.576, + "start": 5.204, + "exec": 1.762, + "clean": 1.018, + "total": 8.575 + }, + "12": { + "dom": 0.016, + "disp": 0.511, + "start": 5.189, + "exec": 1.597, + "clean": 0.886, + "total": 8.2 + } + }, + "stage": { + "dom": { + "values": [ + 0.016, + 0.014, + 0.017, + 0.015, + 0.02, + 0.017, + 0.015, + 0.018, + 0.014, + 0.021, + 0.013, + 0.016 + ], + "mean": 0.016, + "median": 0.016 + }, + "disp": { + "values": [ + 0.581, + 0.675, + 0.578, + 0.467, + 0.627, + 0.516, + 0.462, + 0.608, + 0.518, + 0.839, + 0.576, + 0.511 + ], + "mean": 0.58, + "median": 0.577 + }, + "start": { + "values": [ + 4.878, + 5.334, + 5.065, + 4.796, + 5.964, + 5.865, + 5.304, + 5.74, + 6.002, + 5.442, + 5.204, + 5.189 + ], + "mean": 5.399, + "median": 5.319 + }, + "exec": { + "values": [ + 2.447, + 2.023, + 1.629, + 1.649, + 1.734, + 1.562, + 1.927, + 1.616, + 1.743, + 1.851, + 1.762, + 1.597 + ], + "mean": 1.795, + "median": 1.739 + }, + "clean": { + "values": [ + 1.021, + 1.08, + 1.077, + 1.182, + 1.0, + 1.132, + 0.999, + 0.947, + 1.028, + 1.009, + 1.018, + 0.886 + ], + "mean": 1.032, + "median": 1.019 + }, + "total": { + "values": [ + 8.942, + 9.126, + 8.365, + 8.109, + 9.346, + 9.092, + 8.706, + 8.93, + 9.306, + 9.162, + 8.575, + 8.2 + ], + "mean": 8.822, + "median": 8.936 + } + } + }, + "iterations": 12, + "mean": 8.822, + "total": 105.86, + "date": "2025-12-12T12:03:00", + "memory": 400, + "maxmem": 4000, + "vcpus": 2, + "qrexec_timeout": 60, + "shutdown_timeout": 60, + "kernel": "6.17.9-1.fc41", + "kernelopts": "swiotlb=2048", + "template-buildtime": "2025-09-19 11:46:06", + "last-update": "2025-09-22 11:32:55", + "os": "Linux", + "os-distribution": "debian", + "os-version": 13, + "hcl-qubes": "R4.3-rc3", + "hcl-xen": "4.19.3", + "hcl-kernel": "6.17.9-1", + "hcl-memory": 32404 + }, + "debian-13-xfce_dom0-dispvm-preload-2-api": { + "name": "dom0-dispvm-preload-2-api", + "gui": false, + "concurrent": false, + "from_dom0": true, + "preload_max": 2, + "target_dispvm": true, + "admin_api": true, + "extra_id": "dom0-dispvm-preload-api", + "pretty_name": "Dom0 runs simple app in disposables (2 preloaded) (API)", + "template": "debian-13-xfce", + "api_results": { + "iteration": { + "1": { + "dom": 0.018, + "disp": 0.184, + "exec": 0.19, + "clean": 2.075, + "total": 2.466 + }, + "2": { + "dom": 0.018, + "disp": 0.184, + "exec": 0.102, + "clean": 2.209, + "total": 2.513 + }, + "3": { + "dom": 0.022, + "disp": 1.585, + "exec": 0.14, + "clean": 2.227, + "total": 3.973 + }, + "4": { + "dom": 0.045, + "disp": 0.279, + "exec": 0.126, + "clean": 2.252, + "total": 2.702 + }, + "5": { + "dom": 0.062, + "disp": 1.432, + "exec": 0.125, + "clean": 2.158, + "total": 3.776 + }, + "6": { + "dom": 0.021, + "disp": 0.609, + "exec": 0.139, + "clean": 2.483, + "total": 3.252 + }, + "7": { + "dom": 0.066, + "disp": 1.015, + "exec": 0.165, + "clean": 2.324, + "total": 3.57 + }, + "8": { + "dom": 0.066, + "disp": 1.084, + "exec": 0.149, + "clean": 2.301, + "total": 3.599 + }, + "9": { + "dom": 0.063, + "disp": 1.127, + "exec": 0.133, + "clean": 2.3, + "total": 3.624 + }, + "10": { + "dom": 0.069, + "disp": 0.583, + "exec": 0.117, + "clean": 2.379, + "total": 3.149 + }, + "11": { + "dom": 0.069, + "disp": 0.956, + "exec": 0.127, + "clean": 2.26, + "total": 3.411 + }, + "12": { + "dom": 0.063, + "disp": 1.068, + "exec": 0.182, + "clean": 2.083, + "total": 3.396 + } + }, + "stage": { + "dom": { + "values": [ + 0.018, + 0.018, + 0.022, + 0.045, + 0.062, + 0.021, + 0.066, + 0.066, + 0.063, + 0.069, + 0.069, + 0.063 + ], + "mean": 0.049, + "median": 0.062 + }, + "disp": { + "values": [ + 0.184, + 0.184, + 1.585, + 0.279, + 1.432, + 0.609, + 1.015, + 1.084, + 1.127, + 0.583, + 0.956, + 1.068 + ], + "mean": 0.842, + "median": 0.985 + }, + "exec": { + "values": [ + 0.19, + 0.102, + 0.14, + 0.126, + 0.125, + 0.139, + 0.165, + 0.149, + 0.133, + 0.117, + 0.127, + 0.182 + ], + "mean": 0.141, + "median": 0.136 + }, + "clean": { + "values": [ + 2.075, + 2.209, + 2.227, + 2.252, + 2.158, + 2.483, + 2.324, + 2.301, + 2.3, + 2.379, + 2.26, + 2.083 + ], + "mean": 2.254, + "median": 2.256 + }, + "total": { + "values": [ + 2.466, + 2.513, + 3.973, + 2.702, + 3.776, + 3.252, + 3.57, + 3.599, + 3.624, + 3.149, + 3.411, + 3.396 + ], + "mean": 3.286, + "median": 3.404 + } + } + }, + "iterations": 12, + "mean": 3.286, + "total": 39.433, + "date": "2025-12-12T12:04:34", + "memory": 400, + "maxmem": 4000, + "vcpus": 2, + "qrexec_timeout": 60, + "shutdown_timeout": 60, + "kernel": "6.17.9-1.fc41", + "kernelopts": "swiotlb=2048", + "template-buildtime": "2025-09-19 11:46:06", + "last-update": "2025-09-22 11:32:55", + "os": "Linux", + "os-distribution": "debian", + "os-version": 13, + "hcl-qubes": "R4.3-rc3", + "hcl-xen": "4.19.3", + "hcl-kernel": "6.17.9-1", + "hcl-memory": 32404 + }, + "debian-13-xfce_dom0-dispvm-preload-4-api": { + "name": "dom0-dispvm-preload-4-api", + "gui": false, + "concurrent": false, + "from_dom0": true, + "preload_max": 4, + "target_dispvm": true, + "admin_api": true, + "extra_id": "", + "pretty_name": "Dom0 runs simple app in disposables (4 preloaded) (API)", + "template": "debian-13-xfce", + "api_results": { + "iteration": { + "1": { + "dom": 0.023, + "disp": 0.175, + "exec": 0.088, + "clean": 2.186, + "total": 2.472 + }, + "2": { + "dom": 0.053, + "disp": 0.144, + "exec": 0.108, + "clean": 2.261, + "total": 2.566 + }, + "3": { + "dom": 0.02, + "disp": 0.255, + "exec": 0.16, + "clean": 2.469, + "total": 2.904 + }, + "4": { + "dom": 0.069, + "disp": 0.162, + "exec": 0.345, + "clean": 2.401, + "total": 2.978 + }, + "5": { + "dom": 0.03, + "disp": 0.177, + "exec": 0.096, + "clean": 1.392, + "total": 1.695 + }, + "6": { + "dom": 0.019, + "disp": 1.558, + "exec": 0.229, + "clean": 2.87, + "total": 4.676 + }, + "7": { + "dom": 0.018, + "disp": 0.155, + "exec": 0.122, + "clean": 3.437, + "total": 3.731 + }, + "8": { + "dom": 0.046, + "disp": 0.162, + "exec": 0.145, + "clean": 2.389, + "total": 2.742 + }, + "9": { + "dom": 0.019, + "disp": 0.244, + "exec": 0.189, + "clean": 1.458, + "total": 1.909 + }, + "10": { + "dom": 1.213, + "disp": 0.146, + "exec": 0.267, + "clean": 1.261, + "total": 2.887 + }, + "11": { + "dom": 0.02, + "disp": 1.537, + "exec": 0.169, + "clean": 2.492, + "total": 4.218 + }, + "12": { + "dom": 0.022, + "disp": 0.15, + "exec": 0.088, + "clean": 3.632, + "total": 3.893 + } + }, + "stage": { + "dom": { + "values": [ + 0.023, + 0.053, + 0.02, + 0.069, + 0.03, + 0.019, + 0.018, + 0.046, + 0.019, + 1.213, + 0.02, + 0.022 + ], + "mean": 0.129, + "median": 0.022 + }, + "disp": { + "values": [ + 0.175, + 0.144, + 0.255, + 0.162, + 0.177, + 1.558, + 0.155, + 0.162, + 0.244, + 0.146, + 1.537, + 0.15 + ], + "mean": 0.405, + "median": 0.168 + }, + "exec": { + "values": [ + 0.088, + 0.108, + 0.16, + 0.345, + 0.096, + 0.229, + 0.122, + 0.145, + 0.189, + 0.267, + 0.169, + 0.088 + ], + "mean": 0.167, + "median": 0.152 + }, + "clean": { + "values": [ + 2.186, + 2.261, + 2.469, + 2.401, + 1.392, + 2.87, + 3.437, + 2.389, + 1.458, + 1.261, + 2.492, + 3.632 + ], + "mean": 2.354, + "median": 2.395 + }, + "total": { + "values": [ + 2.472, + 2.566, + 2.904, + 2.978, + 1.695, + 4.676, + 3.731, + 2.742, + 1.909, + 2.887, + 4.218, + 3.893 + ], + "mean": 3.056, + "median": 2.896 + } + } + }, + "iterations": 12, + "mean": 3.056, + "total": 36.671, + "date": "2025-12-12T12:06:10", + "memory": 400, + "maxmem": 4000, + "vcpus": 2, + "qrexec_timeout": 60, + "shutdown_timeout": 60, + "kernel": "6.17.9-1.fc41", + "kernelopts": "swiotlb=2048", + "template-buildtime": "2025-09-19 11:46:06", + "last-update": "2025-09-22 11:32:55", + "os": "Linux", + "os-distribution": "debian", + "os-version": 13, + "hcl-qubes": "R4.3-rc3", + "hcl-xen": "4.19.3", + "hcl-kernel": "6.17.9-1", + "hcl-memory": 32404 + }, + "debian-13-xfce_dom0-dispvm-preload-2-gui-api": { + "name": "dom0-dispvm-preload-2-gui-api", + "gui": true, + "concurrent": false, + "from_dom0": true, + "preload_max": 2, + "target_dispvm": true, + "admin_api": true, + "extra_id": "dom0-dispvm-preload-gui-api", + "pretty_name": "Dom0 runs GUI app in disposables (2 preloaded) (API)", + "template": "debian-13-xfce", + "api_results": { + "iteration": { + "1": { + "dom": 0.015, + "disp": 0.152, + "exec": 1.765, + "clean": 1.494, + "total": 3.426 + }, + "2": { + "dom": 0.015, + "disp": 0.141, + "exec": 1.714, + "clean": 1.554, + "total": 3.424 + }, + "3": { + "dom": 0.029, + "disp": 0.241, + "exec": 1.716, + "clean": 1.832, + "total": 3.819 + }, + "4": { + "dom": 0.025, + "disp": 0.17, + "exec": 1.618, + "clean": 1.866, + "total": 3.679 + }, + "5": { + "dom": 0.032, + "disp": 0.198, + "exec": 1.845, + "clean": 1.63, + "total": 3.705 + }, + "6": { + "dom": 0.016, + "disp": 0.14, + "exec": 2.233, + "clean": 1.288, + "total": 3.677 + }, + "7": { + "dom": 0.016, + "disp": 0.144, + "exec": 1.777, + "clean": 1.891, + "total": 3.828 + }, + "8": { + "dom": 0.019, + "disp": 0.143, + "exec": 1.927, + "clean": 1.654, + "total": 3.743 + }, + "9": { + "dom": 0.043, + "disp": 0.22, + "exec": 1.894, + "clean": 1.681, + "total": 3.838 + }, + "10": { + "dom": 0.016, + "disp": 0.183, + "exec": 1.865, + "clean": 1.485, + "total": 3.549 + }, + "11": { + "dom": 0.02, + "disp": 0.137, + "exec": 1.698, + "clean": 2.102, + "total": 3.957 + }, + "12": { + "dom": 0.027, + "disp": 0.178, + "exec": 1.978, + "clean": 1.7, + "total": 3.883 + } + }, + "stage": { + "dom": { + "values": [ + 0.015, + 0.015, + 0.029, + 0.025, + 0.032, + 0.016, + 0.016, + 0.019, + 0.043, + 0.016, + 0.02, + 0.027 + ], + "mean": 0.023, + "median": 0.019 + }, + "disp": { + "values": [ + 0.152, + 0.141, + 0.241, + 0.17, + 0.198, + 0.14, + 0.144, + 0.143, + 0.22, + 0.183, + 0.137, + 0.178 + ], + "mean": 0.171, + "median": 0.161 + }, + "exec": { + "values": [ + 1.765, + 1.714, + 1.716, + 1.618, + 1.845, + 2.233, + 1.777, + 1.927, + 1.894, + 1.865, + 1.698, + 1.978 + ], + "mean": 1.836, + "median": 1.811 + }, + "clean": { + "values": [ + 1.494, + 1.554, + 1.832, + 1.866, + 1.63, + 1.288, + 1.891, + 1.654, + 1.681, + 1.485, + 2.102, + 1.7 + ], + "mean": 1.681, + "median": 1.667 + }, + "total": { + "values": [ + 3.426, + 3.424, + 3.819, + 3.679, + 3.705, + 3.677, + 3.828, + 3.743, + 3.838, + 3.549, + 3.957, + 3.883 + ], + "mean": 3.711, + "median": 3.724 + } + } + }, + "iterations": 12, + "mean": 3.711, + "total": 44.529, + "date": "2025-12-12T12:07:52", + "memory": 400, + "maxmem": 4000, + "vcpus": 2, + "qrexec_timeout": 60, + "shutdown_timeout": 60, + "kernel": "6.17.9-1.fc41", + "kernelopts": "swiotlb=2048", + "template-buildtime": "2025-09-19 11:46:06", + "last-update": "2025-09-22 11:32:55", + "os": "Linux", + "os-distribution": "debian", + "os-version": 13, + "hcl-qubes": "R4.3-rc3", + "hcl-xen": "4.19.3", + "hcl-kernel": "6.17.9-1", + "hcl-memory": 32404 + }, + "debian-13-xfce_dom0-dispvm-preload-4-gui-api": { + "name": "dom0-dispvm-preload-4-gui-api", + "gui": true, + "concurrent": false, + "from_dom0": true, + "preload_max": 4, + "target_dispvm": true, + "admin_api": true, + "extra_id": "", + "pretty_name": "Dom0 runs GUI app in disposables (4 preloaded) (API)", + "template": "debian-13-xfce", + "api_results": { + "iteration": { + "1": { + "dom": 0.017, + "disp": 0.183, + "exec": 1.741, + "clean": 1.462, + "total": 3.403 + }, + "2": { + "dom": 0.018, + "disp": 0.192, + "exec": 1.94, + "clean": 1.57, + "total": 3.72 + }, + "3": { + "dom": 0.024, + "disp": 0.209, + "exec": 2.207, + "clean": 1.472, + "total": 3.912 + }, + "4": { + "dom": 0.037, + "disp": 0.171, + "exec": 2.418, + "clean": 1.129, + "total": 3.755 + }, + "5": { + "dom": 0.019, + "disp": 0.196, + "exec": 1.932, + "clean": 2.053, + "total": 4.2 + }, + "6": { + "dom": 0.022, + "disp": 0.764, + "exec": 1.849, + "clean": 1.806, + "total": 4.442 + }, + "7": { + "dom": 0.028, + "disp": 0.153, + "exec": 1.73, + "clean": 1.83, + "total": 3.741 + }, + "8": { + "dom": 0.02, + "disp": 0.136, + "exec": 1.71, + "clean": 1.831, + "total": 3.697 + }, + "9": { + "dom": 0.022, + "disp": 0.163, + "exec": 2.422, + "clean": 1.134, + "total": 3.741 + }, + "10": { + "dom": 0.02, + "disp": 0.152, + "exec": 1.992, + "clean": 1.62, + "total": 3.784 + }, + "11": { + "dom": 0.024, + "disp": 0.173, + "exec": 1.817, + "clean": 1.715, + "total": 3.729 + }, + "12": { + "dom": 0.022, + "disp": 0.138, + "exec": 2.053, + "clean": 1.523, + "total": 3.736 + } + }, + "stage": { + "dom": { + "values": [ + 0.017, + 0.018, + 0.024, + 0.037, + 0.019, + 0.022, + 0.028, + 0.02, + 0.022, + 0.02, + 0.024, + 0.022 + ], + "mean": 0.023, + "median": 0.022 + }, + "disp": { + "values": [ + 0.183, + 0.192, + 0.209, + 0.171, + 0.196, + 0.764, + 0.153, + 0.136, + 0.163, + 0.152, + 0.173, + 0.138 + ], + "mean": 0.219, + "median": 0.172 + }, + "exec": { + "values": [ + 1.741, + 1.94, + 2.207, + 2.418, + 1.932, + 1.849, + 1.73, + 1.71, + 2.422, + 1.992, + 1.817, + 2.053 + ], + "mean": 1.984, + "median": 1.936 + }, + "clean": { + "values": [ + 1.462, + 1.57, + 1.472, + 1.129, + 2.053, + 1.806, + 1.83, + 1.831, + 1.134, + 1.62, + 1.715, + 1.523 + ], + "mean": 1.595, + "median": 1.595 + }, + "total": { + "values": [ + 3.403, + 3.72, + 3.912, + 3.755, + 4.2, + 4.442, + 3.741, + 3.697, + 3.741, + 3.784, + 3.729, + 3.736 + ], + "mean": 3.822, + "median": 3.741 + } + } + }, + "iterations": 12, + "mean": 3.822, + "total": 45.86, + "date": "2025-12-12T12:09:40", + "memory": 400, + "maxmem": 4000, + "vcpus": 2, + "qrexec_timeout": 60, + "shutdown_timeout": 60, + "kernel": "6.17.9-1.fc41", + "kernelopts": "swiotlb=2048", + "template-buildtime": "2025-09-19 11:46:06", + "last-update": "2025-09-22 11:32:55", + "os": "Linux", + "os-distribution": "debian", + "os-version": 13, + "hcl-qubes": "R4.3-rc3", + "hcl-xen": "4.19.3", + "hcl-kernel": "6.17.9-1", + "hcl-memory": 32404 + }, + "debian-13-xfce_dom0-vm-api": { + "name": "dom0-vm-api", + "gui": false, + "concurrent": false, + "from_dom0": true, + "preload_max": 0, + "target_dispvm": false, + "admin_api": true, + "extra_id": "", + "pretty_name": "Dom0 runs simple app in another running qube (API)", + "template": "debian-13-xfce", + "api_results": { + "iteration": { + "1": { + "dom": 0.024, + "exec": 0.019, + "total": 0.042 + }, + "2": { + "dom": 0.017, + "exec": 0.017, + "total": 0.035 + }, + "3": { + "dom": 0.016, + "exec": 0.015, + "total": 0.031 + }, + "4": { + "dom": 0.017, + "exec": 0.019, + "total": 0.036 + }, + "5": { + "dom": 0.015, + "exec": 0.022, + "total": 0.037 + }, + "6": { + "dom": 0.014, + "exec": 0.022, + "total": 0.037 + }, + "7": { + "dom": 0.015, + "exec": 0.024, + "total": 0.039 + }, + "8": { + "dom": 0.021, + "exec": 0.031, + "total": 0.051 + }, + "9": { + "dom": 0.018, + "exec": 0.019, + "total": 0.038 + }, + "10": { + "dom": 0.015, + "exec": 0.022, + "total": 0.037 + }, + "11": { + "dom": 0.018, + "exec": 0.028, + "total": 0.046 + }, + "12": { + "dom": 0.023, + "exec": 0.028, + "total": 0.051 + } + }, + "stage": { + "dom": { + "values": [ + 0.024, + 0.017, + 0.016, + 0.017, + 0.015, + 0.014, + 0.015, + 0.021, + 0.018, + 0.015, + 0.018, + 0.023 + ], + "mean": 0.018, + "median": 0.017 + }, + "exec": { + "values": [ + 0.019, + 0.017, + 0.015, + 0.019, + 0.022, + 0.022, + 0.024, + 0.031, + 0.019, + 0.022, + 0.028, + 0.028 + ], + "mean": 0.022, + "median": 0.022 + }, + "total": { + "values": [ + 0.042, + 0.035, + 0.031, + 0.036, + 0.037, + 0.037, + 0.039, + 0.051, + 0.038, + 0.037, + 0.046, + 0.051 + ], + "mean": 0.04, + "median": 0.037 + } + } + }, + "iterations": 12, + "mean": 0.04, + "total": 0.481, + "date": "2025-12-12T12:10:32", + "memory": 400, + "maxmem": 4000, + "vcpus": 2, + "qrexec_timeout": 60, + "shutdown_timeout": 60, + "kernel": "6.17.9-1.fc41", + "kernelopts": "swiotlb=2048", + "template-buildtime": "2025-09-19 11:46:06", + "last-update": "2025-09-22 11:32:55", + "os": "Linux", + "os-distribution": "debian", + "os-version": 13, + "hcl-qubes": "R4.3-rc3", + "hcl-xen": "4.19.3", + "hcl-kernel": "6.17.9-1", + "hcl-memory": 32404 + }, + "debian-13-xfce_dom0-vm-gui-api": { + "name": "dom0-vm-gui-api", + "gui": true, + "concurrent": false, + "from_dom0": true, + "preload_max": 0, + "target_dispvm": false, + "admin_api": true, + "extra_id": "", + "pretty_name": "Dom0 runs GUI app in another running qube (API)", + "template": "debian-13-xfce", + "api_results": { + "iteration": { + "1": { + "dom": 0.018, + "exec": 0.032, + "total": 0.049 + }, + "2": { + "dom": 0.016, + "exec": 0.022, + "total": 0.038 + }, + "3": { + "dom": 0.016, + "exec": 0.022, + "total": 0.038 + }, + "4": { + "dom": 0.025, + "exec": 0.026, + "total": 0.05 + }, + "5": { + "dom": 0.022, + "exec": 0.024, + "total": 0.046 + }, + "6": { + "dom": 0.018, + "exec": 0.021, + "total": 0.039 + }, + "7": { + "dom": 0.022, + "exec": 0.023, + "total": 0.045 + }, + "8": { + "dom": 0.018, + "exec": 0.024, + "total": 0.042 + }, + "9": { + "dom": 0.017, + "exec": 0.02, + "total": 0.037 + }, + "10": { + "dom": 0.017, + "exec": 0.023, + "total": 0.04 + }, + "11": { + "dom": 0.027, + "exec": 0.05, + "total": 0.077 + }, + "12": { + "dom": 0.016, + "exec": 0.021, + "total": 0.036 + } + }, + "stage": { + "dom": { + "values": [ + 0.018, + 0.016, + 0.016, + 0.025, + 0.022, + 0.018, + 0.022, + 0.018, + 0.017, + 0.017, + 0.027, + 0.016 + ], + "mean": 0.019, + "median": 0.018 + }, + "exec": { + "values": [ + 0.032, + 0.022, + 0.022, + 0.026, + 0.024, + 0.021, + 0.023, + 0.024, + 0.02, + 0.023, + 0.05, + 0.021 + ], + "mean": 0.026, + "median": 0.023 + }, + "total": { + "values": [ + 0.049, + 0.038, + 0.038, + 0.05, + 0.046, + 0.039, + 0.045, + 0.042, + 0.037, + 0.04, + 0.077, + 0.036 + ], + "mean": 0.045, + "median": 0.041 + } + } + }, + "iterations": 12, + "mean": 0.045, + "total": 0.538, + "date": "2025-12-12T12:11:09", + "memory": 400, + "maxmem": 4000, + "vcpus": 2, + "qrexec_timeout": 60, + "shutdown_timeout": 60, + "kernel": "6.17.9-1.fc41", + "kernelopts": "swiotlb=2048", + "template-buildtime": "2025-09-19 11:46:06", + "last-update": "2025-09-22 11:32:55", + "os": "Linux", + "os-distribution": "debian", + "os-version": 13, + "hcl-qubes": "R4.3-rc3", + "hcl-xen": "4.19.3", + "hcl-kernel": "6.17.9-1", + "hcl-memory": 32404 + } +} \ No newline at end of file