mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
perf(instances): instance lists read a VM's info once per row
The VM rows of the instance lists used instance.proxy.instance.info in every state check, the vCPU column and the action buttons. A template calls the method on each lookup, so every row made 7 virDomainGetInfo RPCs (8 on a compute's instances page), and the memory column read the domain XML once more. - Instance.info reads virDomainGetInfo once per request; the lists and instance_actions.html use it. - The memory column of both index views comes from the same info (current memory) instead of the domain XML. The value is the same: checked against currentMemory for every running and shut-off VM of the test hosts. A row now makes one info call and one XML read (the title). The new test counts the calls in the grouped and non-grouped index and on the compute instances page; it fails on the previous code with 7 and 8 calls.
This commit is contained in:
parent
10b46afd36
commit
7e08224330
6 changed files with 109 additions and 18 deletions
|
|
@ -87,18 +87,18 @@
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
{% if instance.proxy.instance.info.0 == 1 %}
|
||||
{% if instance.info.0 == 1 %}
|
||||
<span class="text-success">{% trans "Active" %}</span>
|
||||
{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 5 %}
|
||||
{% if instance.info.0 == 5 %}
|
||||
<span class="text-danger">{% trans "Off" %}</span>
|
||||
{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 3 %}
|
||||
{% if instance.info.0 == 3 %}
|
||||
<span class="text-warning">{% trans "Suspended" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ instance.proxy.instance.info.3 }}</td>
|
||||
<td><p class="text-end">{% widthratio instance.proxy.instance.info.1 1024 1 %} MiB</p></td>
|
||||
<td>{{ instance.info.3 }}</td>
|
||||
<td><p class="text-end">{% widthratio instance.info.1 1024 1 %} MiB</p></td>
|
||||
<td class="text-nowrap">
|
||||
{% include 'instance_actions.html' %}
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,11 @@ class Instance(models.Model):
|
|||
def disks(self) -> list[dict]:
|
||||
return self.proxy.get_disk_devices()
|
||||
|
||||
@cached_property
|
||||
def info(self) -> list:
|
||||
"""virDomainGetInfo, read once: state, max memory KiB, memory KiB, vCPUs, CPU time"""
|
||||
return self.proxy.instance.info()
|
||||
|
||||
@cached_property
|
||||
def status(self) -> int:
|
||||
return self.proxy.get_status()
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
{% if instance.proxy.instance.info.0 == 1 %}<span class="text-success">{% trans "Active" %}</span>{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 5 %}<span class="text-danger">{% trans "Off" %}</span>{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 3 %}
|
||||
{% if instance.info.0 == 1 %}<span class="text-success">{% trans "Active" %}</span>{% endif %}
|
||||
{% if instance.info.0 == 5 %}<span class="text-danger">{% trans "Off" %}</span>{% endif %}
|
||||
{% if instance.info.0 == 3 %}
|
||||
<span class="text-warning">{% trans "Suspended" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
|
@ -73,8 +73,8 @@
|
|||
{% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}<span class="text-success">{% else %}<span class="text-danger">{% endif %}{{ instance.drbd }}</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>{{ instance.proxy.instance.info.3 }}</td>
|
||||
<td>{{ instance.cur_memory }} MB</td>
|
||||
<td>{{ instance.info.3 }}</td>
|
||||
<td>{% widthratio instance.info.2 1024 1 %} MB</td>
|
||||
<td class="text-nowrap">
|
||||
{% include 'instance_actions.html' %}
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@
|
|||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if instance.proxy.instance.info.0 == 1 %}<span
|
||||
{% if instance.info.0 == 1 %}<span
|
||||
class="text-success">{% trans "Active" %}</span>{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 5 %}<span
|
||||
{% if instance.info.0 == 5 %}<span
|
||||
class="text-danger">{% trans "Off" %}</span>{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 3 %}<span
|
||||
{% if instance.info.0 == 3 %}<span
|
||||
class="text-warning">{% trans "Suspended" %}</span>{% endif %}
|
||||
</td>
|
||||
{% if app_settings.VM_DRBD_STATUS == 'True' %}
|
||||
|
|
@ -52,8 +52,8 @@
|
|||
{% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}<span class="text-success">{% else %}<span class="text-danger">{% endif %}{{ instance.drbd }}</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>{{ instance.proxy.instance.info.3 }}</td>
|
||||
<td>{{ instance.cur_memory }} MB</td>
|
||||
<td>{{ instance.info.3 }}</td>
|
||||
<td>{% widthratio instance.info.2 1024 1 %} MB</td>
|
||||
<td class="text-nowrap">
|
||||
{% include 'instance_actions.html' %}
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% load i18n %}
|
||||
{% load bootstrap_icons %}
|
||||
<div class="btn-group" role="group" aria-label="Instance actions">
|
||||
{% if instance.proxy.instance.info.0 == 5 %}
|
||||
{% if instance.info.0 == 5 %}
|
||||
{% if instance.is_template %}
|
||||
<a href="{% url 'instances:instance' instance.id %}#clone" class="btn btn-sm btn-secondary" title="{% trans "Clone" %}">
|
||||
{% bs_icon 'copy' %}
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<span>{% bs_icon 'eye-fill' %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 3 %}
|
||||
{% if instance.info.0 == 3 %}
|
||||
<form action="{% url 'instances:resume' instance.id %}" method="post" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-secondary" title="{% trans "Resume" %}">
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
<span>{% bs_icon 'eye-fill' %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 1 %}
|
||||
{% if instance.info.0 == 1 %}
|
||||
<button class="btn btn-sm btn-secondary disabled" title="{% trans "Power On" %}" disabled>{% bs_icon 'play-fill' %}</button>
|
||||
<form action="{% url 'instances:suspend' instance.id %}" method="post" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
|
|
|
|||
86
instances/test_list_rpc.py
Normal file
86
instances/test_list_rpc.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"""Instance lists ask libvirt for a VM's info once per row."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from appsettings.models import AppSettings
|
||||
from computes.models import Compute
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from instances.models import Instance
|
||||
|
||||
# virDomainGetInfo: state, max memory KiB, memory KiB, vCPUs, CPU time
|
||||
INFO = [1, 4194304, 2097152, 3, 0]
|
||||
|
||||
|
||||
class FakeDomain:
|
||||
# a plain class: Django templates do not call MagicMock attributes
|
||||
def __init__(self):
|
||||
self.info_calls = 0
|
||||
self.xml_calls = 0
|
||||
|
||||
def info(self):
|
||||
self.info_calls += 1
|
||||
return INFO
|
||||
|
||||
def XMLDesc(self, flags=0):
|
||||
self.xml_calls += 1
|
||||
return "<domain><title></title><currentMemory>2097152</currentMemory></domain>"
|
||||
|
||||
|
||||
class ListInfoCallTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client.force_login(
|
||||
get_user_model().objects.create_superuser(username="rpc-admin", password="pwd", email="rpc@example.com")
|
||||
)
|
||||
self.compute = Compute.objects.create(name="rpc-compute", hostname="localhost", type=4, login="", password="")
|
||||
Instance.objects.create(compute=self.compute, name="vm-a", uuid="11111111-1111-1111-1111-111111111111")
|
||||
Instance.objects.create(compute=self.compute, name="vm-b", uuid="22222222-2222-2222-2222-222222222222")
|
||||
self.domains = []
|
||||
|
||||
def proxy(self, *args, **kwargs):
|
||||
domain = FakeDomain()
|
||||
self.domains.append(domain)
|
||||
return SimpleNamespace(
|
||||
instance=domain,
|
||||
get_uuid=lambda: "uuid",
|
||||
get_title=lambda: domain.XMLDesc() and "",
|
||||
get_cur_memory=lambda: int(domain.XMLDesc().split(">")[4].split("<")[0]) // 1024,
|
||||
)
|
||||
|
||||
def get(self, url):
|
||||
compute_patches = {"status": True, "cpu_count": 1, "ram_size": 1024, "ram_usage": 0, "cpu_usage": 0}
|
||||
with patch("instances.views.utils.refr"), patch("computes.views.utils.refresh_instance_database"), patch(
|
||||
"instances.models.wvmInstance", side_effect=self.proxy
|
||||
):
|
||||
patches = [patch.object(Compute, k, new_callable=PropertyMock, return_value=v) for k, v in compute_patches.items()]
|
||||
for p in patches:
|
||||
p.start()
|
||||
try:
|
||||
response = self.client.get(url)
|
||||
finally:
|
||||
for p in patches:
|
||||
p.stop()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
def assert_one_info_per_vm(self, response, memory="2048 MB"):
|
||||
self.assertEqual(len(self.domains), 2, "one proxy per VM row")
|
||||
for domain in self.domains:
|
||||
self.assertEqual(domain.info_calls, 1)
|
||||
self.assertEqual(domain.xml_calls, 1, "only the title reads the XML")
|
||||
self.assertContains(response, memory, count=2)
|
||||
self.assertContains(response, "Active")
|
||||
|
||||
def test_nongrouped_index(self):
|
||||
AppSettings.objects.filter(key="VIEW_INSTANCES_LIST_STYLE").update(value="nongrouped")
|
||||
self.assert_one_info_per_vm(self.get(reverse("instances:index")))
|
||||
|
||||
def test_grouped_index(self):
|
||||
AppSettings.objects.filter(key="VIEW_INSTANCES_LIST_STYLE").update(value="grouped")
|
||||
self.assert_one_info_per_vm(self.get(reverse("instances:index")))
|
||||
|
||||
def test_compute_instances(self):
|
||||
# this page shows the maximum memory, as before
|
||||
self.assert_one_info_per_vm(self.get(reverse("instances", args=[self.compute.id])), memory="4096 MiB")
|
||||
Loading…
Add table
Add a link
Reference in a new issue