Skip to content

Report more statistics#827

Open
ben-grande wants to merge 10 commits into
QubesOS:mainfrom
ben-grande:stat-cpu
Open

Report more statistics#827
ben-grande wants to merge 10 commits into
QubesOS:mainfrom
ben-grande:stat-cpu

Conversation

@ben-grande

@ben-grande ben-grande commented Jun 11, 2026

Copy link
Copy Markdown
Contributor
  • online vcpus
  • time of each vcpu
  • share all vm_info captured with clients (I presume there is no intent to capture private information internally and not broadcast to clients, as the only place where get_vm_stats() is called is from admin.vm.Stats.

Requires:

@marmarek marmarek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the breaking change regarding CPU usage. Is that really that useful to risk breaking many other things (you found one, but there may be more, including 3rd-party you have no way of knowing)?
If you really want to do that, better include new field with higher precision, but I'd prefer you don't.

Comment thread qubes/app.py Outdated

- memory_kb - current memory assigned, in kb
- memory_used_kb - current memory that can be used, in kb
- maxmem_kb - maximum memory assigned, in kb

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you probably intend to have here is just maxmem property (so, no need to include it in the stats). But what you actually get here will be closer to the current assignment, and will actually change as qmemman does its work.

@ben-grande ben-grande Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The domain cannot exceed the memory qmemman assigned to it, so for a moment (or a really long time when there is no available memory to redistribute), that is really the memory the domain can try to use at maximum. Is there a problem of reporting both?

Edit:

Note it may be bigger than actually assigned memory (in which case some swap is likely in use).

Hum... yes, then I think we can't rely on the method I proposed.

@ben-grande ben-grande Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just maxmem property (so, no need to include it in the stats)

That can change while the domain is running and the client might be too late to initialize the cache. Stubdomains don't have that property. I think that I should set maxmem_kb the same as mem_kb of app.vmm.xc.domain_getinfo() and the same as the qube.libvirt_domain.info()[2], which doesn't change while qube is running.

Yes, this means that QubesVM.get_mem_static_max with self.libvirt_domain.maxMemory() is wrong, cause it should have been self.libvirt_domain.info()[2] or self.libvirt_domain.memoryStats()["actual"], but only for non-memory balanced domains.

ahhhhhhhh. maybe I just need to cached maxmem on the server when the domain starts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can I know which keys a domain can write to xenstore? When I am reading xenstore of a qube, should I assume that a domain can change any key? It can change memory/meminfo, but can it change memory/static-max?

@ben-grande ben-grande Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import qubes
app = qubes.Qubes()
domains = app.domains
def gs(name):
    qube = domains[name]
    print("libvirt:")
    print("  info:", qube.libvirt_domain._vm.info())
    print("  maxMemory:", qube.libvirt_domain._vm.maxMemory())
    print("  memoryStats:", qube.libvirt_domain._vm.memoryStats())
    print("xen:")
    print("  domain_getinfo:", qube.app.vmm.xc.domain_getinfo(qube.xid, 1))
    print("  xenstore:", {i: qube.app.vmm.xs.read("", "/local/domain/{}/memory/{}".format(qube.xid, i)) for i in qube.app.vmm.xs.ls("", "/local/domain/{}/memory".format(qube.xid))})

while True:
    try:
        gs(input("Write qube name: "))
    except KeyError:
        print("Qube doesn't exist")
summary markdown

  • dom0 (balanced):

    • xen - domain_getinfo:
      • mem_kb: 4177920 kB = 4080 MiB
      • maxmem_kb: 4195328 kB = 4097 MiB
    • xen - xs.read:
      • static-max: 4194304 kB = 4096 MiB
      • meminfo: 2144108 kB = 2093 MiB
      • target: 4177920 kB = 4080 MiB
    • libvirt - info:
      • [1]: 33182058 kB = 32404 MiB
      • [2]: 4177920 kB = 4080 MiB
    • libvirt - memoryStats:
      • actual: 4177920 kB = 4080 MiB
      • available: 33182058 kB = 32404 MiB
    • libvirt - maxMemory:
      • result: 33182058 kB = 32404 MiB
  • sys-net (not balanced):

    • qube:
      • memory: 512000 kB = 500 MiB
      • maxmem: 0 kB = 0 MiB
    • xen - domain_getinfo:
      • mem_kb: 495660 kB = 484 MiB
      • maxmem_kb: 513024 kB = 501 MiB
    • xen - xs.read:
      • statix-max: 512000 kB = 500 MiB
      • target: 495616 kB = 484 MiB
      • videoram: 16384 kB = 16 MiB
    • libvirt - info:
      • [1]: 512000 kB = 500 MiB
      • [2]: 495660 kB = 484 MiB
    • libvirt - memoryStats:
      • actual: 495660 kB = 484 MiB
      • available: 512000 kB = 500 MiB
    • libvirt - maxMemory:
      • result: 512000 kB = 500 MiB
  • test (balanced, using swap, mod memory prop to 600MiB when running):

    • qube:
      • memory: 409600 kB = 400 MiB
      • maxmem: 512000 kB = 500 MiB
    • xen - domain_getinfo:
      • mem_kb: 495672 kB = 484 MiB
      • maxmem_kb: 513024 kB = 501 MiB
    • xen - xs.read:
      • statix-max: 512000 kB = 500 MiB
      • target: 495616 kB = 484 MiB
      • videoram: 0 kB = 0 MiB
      • meminfo: 847892 kB = 828 MiB
      • hotplug-max: 512000 kB = 500 MiB
    • libvirt - info:
      • [1]: 409600 kB = 400 MiB
      • [2]: 495672 kB = 484 MiB
    • libvirt - memoryStats:
      • actual: 495672 kB = 484 MiB
      • available: 409600 kB = 400 MiB
    • libvirt - maxMemory:
      • result: 409600 kB = 400 MiB
  • dispX (balanced, preloaded disposable, qmemman just restricted memory for pause:

    • qube:
      • memory: 409600 kB = 400 MiB
      • maxmem: 4096000 kB = 4000 MiB
    • xen - domain_getinfo:
      • mem_kb: 434444 kB = 424 MiB
      • maxmem_kb: 423860 kB = 413 MiB
    • xen - xs.read:
      • static-max: 422838 kB = 412 MiB
      • target: 406454 kB = 396 MiB
      • videoram: 0 kB = 0 MiB
      • meminfo: 325260 kB = 317 MiB
      • hotplug-max: 4096000 kB = 4000 MiB
    • libvirt - info:
      • [1]: 409600 kB = 400 MiB
      • [2]: 434444 kB = 424 MiB
    • libvirt - memoryStats:
      • actual: 434444 kB = 424 MiB
      • available: 409600 kB = 400 MiB
    • libvirt - maxMemory:
      • result: 409600 kB = 400 MiB

summary with indented fields

- dom0 (balanced):
    - xen - domain_getinfo:
        - mem_kb:      4177920 kB =  4080 MiB
        - maxmem_kb:   4195328 kB =  4097 MiB
    - xen - xs.read:
        - static-max:  4194304 kB =  4096 MiB
        - meminfo:     2144108 kB =  2093 MiB
        - target:      4177920 kB =  4080 MiB
    - libvirt - info:
        - [1]:        33182058 kB = 32404 MiB
        - [2]:         4177920 kB =  4080 MiB
    - libvirt - memoryStats:
        - actual:      4177920 kB =  4080 MiB
        - available:  33182058 kB = 32404 MiB
    - libvirt - maxMemory:
        - result:     33182058 kB = 32404 MiB

- sys-net (not balanced):
    - qube:
        - memory:       512000 kB =   500 MiB
        - maxmem:            0 kB =     0 MiB
    - xen - domain_getinfo:
        - mem_kb:       495660 kB =   484 MiB
        - maxmem_kb:    513024 kB =   501 MiB
    - xen - xs.read:
        - statix-max:   512000 kB =   500 MiB
        - target:       495616 kB =   484 MiB
        - videoram:      16384 kB =    16 MiB
    - libvirt - info:
        - [1]:          512000 kB =   500 MiB
        - [2]:          495660 kB =   484 MiB
    - libvirt - memoryStats:
        - actual:       495660 kB =   484 MiB
        - available:    512000 kB =   500 MiB
    - libvirt - maxMemory:
        - result:       512000 kB =   500 MiB

- test (balanced, using swap, mod memory prop to 600MiB when running):
    - qube:
        - memory:       409600 kB =   400 MiB
        - maxmem:       512000 kB =   500 MiB
    - xen - domain_getinfo:
        - mem_kb:       495672 kB =   484 MiB
        - maxmem_kb:    513024 kB =   501 MiB
    - xen - xs.read:
        - statix-max:   512000 kB =   500 MiB
        - target:       495616 kB =   484 MiB
        - videoram:          0 kB =     0 MiB
        - meminfo:      847892 kB =   828 MiB
        - hotplug-max:  512000 kB =   500 MiB
    - libvirt - info:
        - [1]:          409600 kB =   400 MiB
        - [2]:          495672 kB =   484 MiB
    - libvirt - memoryStats:
        - actual:       495672 kB =   484 MiB
        - available:    409600 kB =   400 MiB
    - libvirt - maxMemory:
        - result:       409600 kB =   400 MiB

- dispX (balanced, preloaded disposable, qmemman just restricted memory for pause:
    - qube:
        - memory:       409600 kB =   400 MiB
        - maxmem:      4096000 kB =  4000 MiB
    - xen - domain_getinfo:
        - mem_kb:       434444 kB =   424 MiB
        - maxmem_kb:    423860 kB =   413 MiB
    - xen - xs.read:
        - static-max:   422838 kB =   412 MiB
        - target:       406454 kB =   396 MiB
        - videoram:          0 kB =     0 MiB
        - meminfo:      325260 kB =   317 MiB
        - hotplug-max: 4096000 kB =  4000 MiB
    - libvirt - info:
        - [1]:          409600 kB =   400 MiB
        - [2]:          434444 kB =   424 MiB
    - libvirt - memoryStats:
        - actual:       434444 kB =   424 MiB
        - available:    409600 kB =   400 MiB
    - libvirt - maxMemory:
        - result:       409600 kB =   400 MiB

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, uhm, if you want get_mem to be memory usage, it has to read xenstore meminfo, else, I change the docstring to be "memory assignment" and calculate the correct value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the system perspective it makes more sense to have what is allocated to that qube (how much host memory it actually uses), not what the qube would want. So, libvirt memoryStats()['actual'].

Github didn't load this reply for me until I reloaded the page.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the server broadcast how much memory is assigned to the domain, for app.vmm.xc.domain_get_info(), this is mem_kb and maxmem_kb. I believe that mem_kb is memory qmemman has assigned to the domain while is maxmem_kb is mem_kb+videoram+EPT-or-whatever, because videoram is 16MiB and the EPT is ~1MiB. I think I will broadcast both values.

>>> {m: app.vmm.xs.read("", "/local/domain/0/memory/" + m) for m in app.vmm.xs.ls("", "/local/domain/0/memory")}
{'static-max': b'4194304', 'meminfo': b'1864900', 'target': b'4177920'}
>>> d0 = app.vmm.xc.domain_getinfo(0,1)[0]; {'maxmem_kb': d0['maxmem_kb'], 'mem_kb': d0['mem_kb']}
{'maxmem_kb': 4195328, 'mem_kb': 4177920}
>>> i = dom0.libvirt_domain.info(); {'maxmem_kb': i[1], 'mem_kb': i[1+1]}
{'maxmem_kb': 33182508, 'mem_kb': 4177920}
>>>
>>> {m: app.vmm.xs.read("", "/local/domain/38/memory/" + m) for m in app.vmm.xs.ls("", "/local/domain/38/memory")}
{'static-max': b'4096000', 'target': b'2556453', 'videoram': b'16384', 'meminfo': b'482496', 'swapinfo': b''}
>>> da = app.vmm.xc.domain_getinfo(38,1)[0]; {'maxmem_kb': da['maxmem_kb'], 'mem_kb': da['mem_kb']}
{'maxmem_kb': 2573860, 'mem_kb': 2556496}
>>> i = doma.libvirt_domain.info(); {'maxmem_kb': i[1], 'mem_kb': i[1+1]}
{'maxmem_kb': 4096000, 'mem_kb': 2556496}

Libvirt is very discrepant on maxmem_kb compared to Xen, most likely because it doesn't update when memory is redistributed. So I am here again, asking to broadcast both maxmem_kib and mem_kb. Use case:

  • how much memory the qube can claim at any time: up to mem_kib
  • how much memory the qube is consuming from the system (including everything known, videoram, EPT and whatever may come: maxmem_kib.

Example.

With pressure:

good-top

No pressure:

good-second-top
  • MS: set/assigned
  • MM: maxmem

Notice that for dom0, gray, no-guivm, orange, sys-firewall, MS never reaches MM, because it is using mem_kib. For aaaa I allowed to show maxmem_kib, and it is one MiB over because Xen wants, as discussed elsewhere.

So, giving real examples of what I think is important to show

  • A summary of how much memory the system has and how much it is using (sum of all maxmem_kib).
  • How much memory the qube is using divided by how much it has asigned: meminfo / mem_kib.

Because if I don't broadcast both values, things will be wrong. And how wrong? As wrong as videoram+EPT * memory-balanced-qubes.

I cannot use mamxmem property to get how much memory the qubes are using from the system, it will be wrong, as that value doesn't represent how much memory is really available to a qube.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets focus on what would be useful and easy to understand. For an average user, not somebody who spent a month studying the subject.
I would say, a current total memory usage of a qube is an interesting value, because it shows where your overall system RAM got allocated (ideally, those values should sum up to your RAM size).
A second interesting value would be something answering "is a qube is running out of memory?". That would be likely meminfo / mem_kb as you said above.
Those two will not match the numbers you see in top inside a qube, but I would say that's okay, as that doesn't match on bare meta either (for example, a bare metal system with 96GiB shows total of 96440 MiB total memory, which is just above 94GiB).

Maybe some advanced view that shows overhead size of each qube (either as separate overhead size, or as memory usable for the qube itself) would be useful for somebody, but most of the time IMO it would be more confusing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question was "is it okay to broadcast?". Per your response, it seems to be fine, you are just worried of pumping too much information in the viewer. By default, qvm-top doesn't show that many columns. I will take your comments into consideration in the documentation of each column, and to consider which ones should be enabled by default and how they relate to each other.

Comment thread qubes/app.py Outdated
where measurements is a dictionary with key: domid, value: dict:

- memory_kb - current memory assigned, in kb
- memory_used_kb - current memory that can be used, in kb

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "can be used"? This is what qube itself reports as usage. Note it may be bigger than actually assigned memory (in which case some swap is likely in use).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assiged memory=400, maxmem=500 to a qube, started it and opened firefox, memoy_usage=140.0. Do you think I should cap it to 100.0? I don't think any number above 100.0 is informative, at that is already critical, as swap is slower and dom0 is not aware of how much swap the qube has available, so can't calculate.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the above means it wants to use 700MB? Or 560MB? Percentage of what? IMO better report a size here, and let the frontend calculate whatever percentage it wants. For example, if it's above memory_kb but below maxmem it may show different info than when it's above maxmem - in the latter case, user may want to increase maxmem, but in the former just close some other qube to free memory.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the above means it wants to use 700MB? Or 560MB? Percentage of what?

Percentage is meminfo / maxmem. The value exceeded because qube was using most of its memory and swap, so while maxmem was 500MiB, swap was 1G and by using swap, meminfo was way over maxmem.

Comment thread qubes/app.py Outdated
if self.app.vmm.is_xen and (
stubdom_id := self.app.vmm.xs.read(
"",
"/local/domain/{}/image/device-model-domid".format(domid),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't change while a VM is running. Maybe cache it, by including in the returned dict, so the next iteration will give it back as previous argument?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added cache in #819. I am testing QubesOS/qubes-core-admin-client#333 with a merge of #827 and #819.

Comment thread qubes/api/admin.py Outdated
Comment on lines +2517 to +2518
new_vm_info = vm_info.copy()
new_vm_info["cpu_time"] = int(new_vm_info["cpu_time"] / 1000000)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it's shorter this way, it hides what is actually being sent, making it easy to accidentally change the output format.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped this commit.

Comment thread qubes/app.py Outdated
info = {v["domid"]: v for v in info}
for domid in info.keys():
stubdom_id = None
if domid in stubdoms:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check will work only if target domain is processed before it's stubdomain. Currently stubdomain has its ID one bigger than its target, but I'm not sure if that's really guaranteed, and not just a coincidence (for example, it might not be the case if two HVMs are started in parallel in the exact same time).
Plus, this is working only because nowadays dicts in python preserve order on iteration and the domain_getinfo() API happens to return info sorted. But, it's rather fragile, easy to break when doing some changes here. At the very least, add a comment about assumptions of this line. But maybe, at the very least add also explicit sorting of info.keys() (should be cheap on already sorted input)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am having to query the domain name to check the domain property maxmem (I need to access the name because we have domid only, which is xid, which is different than qid, so can't check presence in collection using domid/xid if not looping through every domain anyway. Therefore now I am using the VMCollection and will use stubdom_xid for this, instead of assuming upstream stays the same.

@marmarek marmarek Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, please don't include maxmem in the stats output. Application if needed can access it itself, and if it needs updates can also listen for events (so no need for polling yet another thing). Keep only really dynamic info in the stats output.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no more assumption about order. It may come at any order, and although it may not enter that check, there are extra checks later when more information is available to determine if qube is stubdom.

@marmarek

Copy link
Copy Markdown
Member

BTW, currently returned fields are documented at https://doc.qubes-os.org/en/r4.3/developer/services/admin-api.html, that will need an update if you add more.

@ben-grande

ben-grande commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

We have a problem with units, some are kB and others are MiB: QubesOS/qubes-issues#1737. Unfortunately, a memory stat is already being reported as memory_kb. Should I report some of the properties in kB and others in MiB? Will that make it easier for a future migration, when everything can simply use MiB?

@marmarek

Copy link
Copy Markdown
Member

We have a problem with units

IMO all usage stats should be in KiB, to be accurate. It might not matter that much for bigger qubes, but it does matter for smaller ones. The technical assignment unit is memory page (4k). If one day memory ballooning would support operation on super pages (2MiB), I would be very happy (and then usage could be reported in MiB too), but that's not going to happen in practice ever...

For configuration values we have MiB, as that's more convenient for the user.

@ben-grande
ben-grande force-pushed the stat-cpu branch 7 times, most recently from bf0547b to 6ee911b Compare June 17, 2026 15:04
@ben-grande
ben-grande force-pushed the stat-cpu branch 4 times, most recently from aa4e10d to 5c63d74 Compare June 18, 2026 13:15
@ben-grande

Copy link
Copy Markdown
Contributor Author

For HVMs, there is videoram:

>>> net.name
'sys-net'
>>> domid = net.xid; {i: app.vmm.xs.read("", "/local/domain/{}/memory/{}".format(domid, i)) for i in app.vmm.xs.ls("", "/local/domain/{}/memory".format(domid))}
{'static-max': b'512000', 'target': b'495616', 'videoram': b'16384'}
>>> # For libvirt, I unfortunately only found it in XMLDesc()
>>> net_xml.find(".//video/model[@type='vga']").get("vram")
'16384'
>>> 512000 - 495616
16384
>>> net.libvirt_domain._vm.info()
[1, 512000, 495660, 2, 319197464138]
>>> net.libvirt_domain._vm.memoryStats()
{'actual': 495660, 'available': 512000}

So videoram is not considered for redistribution, cool. But if we want to really know how much memory is reserved for that qube, we want to know the memoryStats()["available"] or static-max.

@ben-grande
ben-grande force-pushed the stat-cpu branch 2 times, most recently from bed7c65 to 9ed126d Compare June 19, 2026 10:59
@ben-grande
ben-grande force-pushed the stat-cpu branch 2 times, most recently from 379cd6e to b9667d1 Compare July 22, 2026 13:10
@ben-grande

Copy link
Copy Markdown
Contributor Author

Caching if key has a value to avoid extraneous xs.read

Testing 100 iterations.

No profiler

['0.0066', '0.0013', '0.0011', '0.0012', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0010', '0.0011', '0.0011', '0.0011', '0.0011', '0.0012', '0.0011', '0.0011', '0.0010', '0.0010', '0.0010', '0.0009', '0.0009', '0.0008', '0.0008', '0.0008', '0.0008', '0.0008', '0.0008', '0.0008', '0.0008', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0008', '0.0010', '0.0010', '0.0010', '0.0010', '0.0010', '0.0012', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0010', '0.0011', '0.0010', '0.0010', '0.0010', '0.0009', '0.0009', '0.0008', '0.0008', '0.0008', '0.0008', '0.0008', '0.0008', '0.0007', '0.0007', '0.0008', '0.0007', '0.0007', '0.0007', '0.0007', '0.0008', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007', '0.0007']

  • 1st: 0.0066
  • Max: 0.0066
  • Min: 0.0007
  • Avg: 0.0010
  • Med: 0.0009

cProfile

cProfile

         24252 function calls in 0.115 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      100    0.001    0.000    0.115    0.001 stats.py:12(f)
      100    0.009    0.000    0.114    0.001 app.py:392(get_vm_stats)
     3200    0.004    0.000    0.083    0.000 app.py:588(query_xs_memory)
     1255    0.081    0.000    0.081    0.000 {method 'read' of 'xen.lowlevel.xs.xs' objects}
      100    0.015    0.000    0.015    0.000 {method 'domain_getinfo' of 'xen.lowlevel.xc.xc' objects}
     1980    0.001    0.000    0.002    0.000 app.py:568(calculate_cpu_usage_raw)
       20    0.000    0.000    0.002    0.000 app.py:2191(get_name_from_domid)
     3111    0.001    0.000    0.001    0.000 app.py:241(is_xen)
       15    0.000    0.000    0.001    0.000 app.py:579(query_stubdom_xid)
     1255    0.000    0.000    0.001    0.000 app.py:246(xs)
      100    0.001    0.000    0.001    0.000 {built-in method builtins.sorted}
     3960    0.001    0.000    0.001    0.000 {method 'get' of 'dict' objects}
     1980    0.001    0.000    0.001    0.000 {built-in method builtins.round}
     1204    0.000    0.000    0.000    0.000 {method 'isdigit' of 'bytes' objects}
     3111    0.000    0.000    0.000    0.000 app.py:196(init_vmm_connection)
     2000    0.000    0.000    0.000    0.000 app.py:608(<lambda>)
      120    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
      100    0.000    0.000    0.000    0.000 app.py:258(xc)
      200    0.000    0.000    0.000    0.000 {built-in method time.perf_counter_ns}
      100    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
      100    0.000    0.000    0.000    0.000 {built-in method time.time}
      100    0.000    0.000    0.000    0.000 {method 'keys' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       20    0.000    0.000    0.000    0.000 {method 'decode' of 'bytes' objects}
       20    0.000    0.000    0.000    0.000 {method 'endswith' of 'str' objects}


['0.0060', '0.0014', '0.0012', '0.0012', '0.0012', '0.0012', '0.0012', '0.0012', '0.0012', '0.0012', '0.0014', '0.0014', '0.0012', '0.0012', '0.0012', '0.0012', '0.0012', '0.0012', '0.0012', '0.0012', '0.0011', '0.0012', '0.0011', '0.0012', '0.0012', '0.0012', '0.0012', '0.0011', '0.0012', '0.0013', '0.0012', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0010', '0.0010', '0.0010', '0.0010', '0.0010', '0.0010', '0.0010', '0.0013', '0.0012', '0.0012', '0.0011', '0.0011', '0.0011', '0.0012', '0.0011', '0.0010', '0.0010', '0.0010', '0.0010', '0.0010', '0.0010', '0.0010', '0.0010', '0.0011', '0.0011', '0.0011', '0.0012', '0.0012', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0012', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011', '0.0010', '0.0010', '0.0010', '0.0010', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0011', '0.0012']

  • 1st: 0.0060
  • Max: 0.0060
  • Min: 0.0009
  • Avg: 0.0011
  • Med: 0.0011

Before this caching of key state:

      100    0.014    0.000    0.224    0.002 app.py:392(get_vm_stats)
     2835    0.185    0.000    0.185    0.000 {method 'read' of 'xen.lowlevel.xs.xs' objects}

After the caching:

      100    0.009    0.000    0.114    0.001 app.py:392(get_vm_stats)
     3200    0.004    0.000    0.083    0.000 app.py:588(query_xs_memory)
     1255    0.081    0.000    0.081    0.000 {method 'read' of 'xen.lowlevel.xs.xs' objects}

1 iteration

cProfile

         591 function calls in 0.006 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.006    0.006 stats.py:12(f)
        1    0.000    0.000    0.006    0.006 app.py:392(get_vm_stats)
       67    0.004    0.000    0.004    0.000 {method 'read' of 'xen.lowlevel.xs.xs' objects}
       32    0.000    0.000    0.002    0.000 app.py:588(query_xs_memory)
       20    0.000    0.000    0.002    0.000 app.py:2191(get_name_from_domid)
       15    0.000    0.000    0.001    0.000 app.py:579(query_stubdom_xid)
        1    0.001    0.001    0.001    0.001 {method 'domain_getinfo' of 'xen.lowlevel.xc.xc' objects}
      141    0.000    0.000    0.000    0.000 app.py:241(is_xen)
       67    0.000    0.000    0.000    0.000 app.py:246(xs)
       21    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
      141    0.000    0.000    0.000    0.000 app.py:196(init_vmm_connection)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       20    0.000    0.000    0.000    0.000 {method 'decode' of 'bytes' objects}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.sorted}
       20    0.000    0.000    0.000    0.000 {method 'endswith' of 'str' objects}
       16    0.000    0.000    0.000    0.000 {method 'isdigit' of 'bytes' objects}
       20    0.000    0.000    0.000    0.000 app.py:608(<lambda>)
        1    0.000    0.000    0.000    0.000 app.py:258(xc)
        2    0.000    0.000    0.000    0.000 {built-in method time.perf_counter_ns}
        1    0.000    0.000    0.000    0.000 {built-in method time.time}
        1    0.000    0.000    0.000    0.000 {method 'keys' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}


['0.0055']

  • 1st: 0.0055
  • Max: 0.0055
  • Min: 0.0055
  • Avg: 0.0055
  • Med: 0.0055

2 iterations

cProfile

         830 function calls in 0.009 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        2    0.000    0.000    0.009    0.004 stats.py:12(f)
        2    0.000    0.000    0.008    0.004 app.py:392(get_vm_stats)
       79    0.007    0.000    0.007    0.000 {method 'read' of 'xen.lowlevel.xs.xs' objects}
       64    0.000    0.000    0.004    0.000 app.py:588(query_xs_memory)
       20    0.000    0.000    0.002    0.000 app.py:2191(get_name_from_domid)
       15    0.000    0.000    0.001    0.000 app.py:579(query_stubdom_xid)
        2    0.001    0.000    0.001    0.000 {method 'domain_getinfo' of 'xen.lowlevel.xc.xc' objects}
      171    0.000    0.000    0.000    0.000 app.py:241(is_xen)
       79    0.000    0.000    0.000    0.000 app.py:246(xs)
       22    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
       28    0.000    0.000    0.000    0.000 {method 'isdigit' of 'bytes' objects}
       20    0.000    0.000    0.000    0.000 app.py:568(calculate_cpu_usage_raw)
      171    0.000    0.000    0.000    0.000 app.py:196(init_vmm_connection)
        2    0.000    0.000    0.000    0.000 {built-in method builtins.sorted}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       40    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
       20    0.000    0.000    0.000    0.000 {method 'decode' of 'bytes' objects}
       20    0.000    0.000    0.000    0.000 {built-in method builtins.round}
       20    0.000    0.000    0.000    0.000 {method 'endswith' of 'str' objects}
       40    0.000    0.000    0.000    0.000 app.py:608(<lambda>)
        4    0.000    0.000    0.000    0.000 {built-in method time.perf_counter_ns}
        2    0.000    0.000    0.000    0.000 app.py:258(xc)
        2    0.000    0.000    0.000    0.000 {built-in method time.time}
        2    0.000    0.000    0.000    0.000 {method 'keys' of 'dict' objects}
        2    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}


['0.0071', '0.0014']

  • 1st: 0.0071
  • Max: 0.0071
  • Min: 0.0014
  • Avg: 0.0043
  • Med: 0.0043

1 iteration:

       67    0.004    0.000    0.004    0.000 {method 'read' of 'xen.lowlevel.xs.xs' objects}

2 iterations

       79    0.007    0.000    0.007    0.000 {method 'read' of 'xen.lowlevel.xs.xs' objects}

79 - 67 = 12, which is the amount of memory balanced domains. Swapinfo is not available in any running qube, else it would double the calls to 24 calls.

If we get the number I estimated above that is spent on a single xs.read and multiple by the number of new calls:

>>> "{:.6f}".format(0.000065 * 12)
'0.000780'
>>> "{:.6f}".format(0.000065 * 12 * 2)
'0.001560'

For each memory balanced qube, we'd spend 0.13ms on xs.read:

>>> "{:.6f}".format(0.000065 * 1 * 2)
'0.000130'

While for 50 memory balanced qubes, we'd spend 6.5ms on xs.read:

>>> "{:.6f}".format(0.000065 * 50 * 2)
'0.006500'

Comment thread qubes/app.py Outdated
"xs_swapinfo", None
)
stubdom_xid = current[domid]["stubdom_xid"]
if stubdom_xid > 0 and stubdom_xid not in info:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What case is it? It isn't possible that stubdom restarts while its target remains the same - they live together and die together. Since stubdom_xid field is tied to target xid (domid variable), you need to fetch it just once, and no need to refresh it ever (as long as that domid is alive).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My wrong analysis.

Comment thread qubes/vm/qubesvm.py
self.libvirt_domain.createWithFlags(
libvirt.VIR_DOMAIN_START_PAUSED
)
self.create_xs_entries()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please say in the commit message why moving it to a separate function, called earlier.

@ben-grande

Copy link
Copy Markdown
Contributor Author

Rectifying the time taken before this PR, as I didn't consider some handling on qubes.api.admin: self.app.vmm.libvirt_conn.lookupByID(vm_id).name(). I moved that to QubesHost.get_vm_stats and cached the first time name is sent. This repo is qubes-core-admin-orig. The branch of my PR is qubes-core-admin.

10 iterations. 20 domains running, 4 HVMs and 4 stubdomains, 12 normal.


Before my PR

No profiler

% PYTHONPATH=~/qubes-core-admin-orig python3 -c 'import stats; stats.fl(count=10, profiler="")'

['0.0043', '0.0003', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002']

  • 1st: 0.0043
  • Max: 0.0043
  • Min: 0.0002
  • Avg: 0.0006
  • Med: 0.0002

cProfile

% PYTHONPATH=~/qubes-core-admin-orig python3 -c 'import stats; stats.fl(count=10, profiler="cProfile")'
cProfile

         1820 function calls in 0.007 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       10    0.000    0.000    0.007    0.001 stats.py:12(f)
       10    0.000    0.000    0.007    0.001 app.py:379(get_vm_stats)
       18    0.000    0.000    0.003    0.000 app.py:148(wrapper)
       18    0.000    0.000    0.003    0.000 libvirt.py:5025(lookupByID)
       18    0.003    0.000    0.003    0.000 {built-in method libvirtmod.virDomainLookupByID}
       10    0.002    0.000    0.002    0.000 {method 'domain_getinfo' of 'xen.lowlevel.xc.xc' objects}
       33    0.000    0.000    0.000    0.000 functools.py:36(update_wrapper)
       18    0.000    0.000    0.000    0.000 app.py:141(__getattr__)
       15    0.000    0.000    0.000    0.000 app.py:99(__getattr__)
      522    0.000    0.000    0.000    0.000 {built-in method builtins.round}
       15    0.000    0.000    0.000    0.000 libvirt.py:541(__del__)
      312    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
       15    0.000    0.000    0.000    0.000 {built-in method libvirtmod.virDomainFree}
       48    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
       15    0.000    0.000    0.000    0.000 app.py:104(wrapper)
      198    0.000    0.000    0.000    0.000 {built-in method builtins.setattr}
       33    0.000    0.000    0.000    0.000 <frozen abc>:117(__instancecheck__)
      180    0.000    0.000    0.000    0.000 {built-in method builtins.max}
       15    0.000    0.000    0.000    0.000 libvirt.py:2197(name)
       15    0.000    0.000    0.000    0.000 app.py:136(_wrap_domain)
       33    0.000    0.000    0.000    0.000 functools.py:66(wraps)
       10    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
       15    0.000    0.000    0.000    0.000 libvirt.py:535(__init__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       33    0.000    0.000    0.000    0.000 {built-in method _abc._abc_instancecheck}
       15    0.000    0.000    0.000    0.000 {built-in method libvirtmod.virDomainGetName}
       20    0.000    0.000    0.000    0.000 app.py:241(is_xen)
       18    0.000    0.000    0.000    0.000 app.py:223(libvirt_conn)
        3    0.000    0.000    0.000    0.000 libvirt.py:45(__init__)
       33    0.000    0.000    0.000    0.000 {method 'update' of 'dict' objects}
       10    0.000    0.000    0.000    0.000 app.py:258(xc)
        3    0.000    0.000    0.000    0.000 app.py:125(_reconnect_if_dead)
       20    0.000    0.000    0.000    0.000 {built-in method time.perf_counter_ns}
        3    0.000    0.000    0.000    0.000 libvirt.py:4896(isAlive)
       38    0.000    0.000    0.000    0.000 app.py:196(init_vmm_connection)
       10    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        3    0.000    0.000    0.000    0.000 {built-in method libvirtmod.virConnectIsAlive}
       10    0.000    0.000    0.000    0.000 {built-in method time.time}
        3    0.000    0.000    0.000    0.000 {built-in method libvirtmod.virGetLastError}
       15    0.000    0.000    0.000    0.000 app.py:76(__init__)
        3    0.000    0.000    0.000    0.000 libvirt.py:59(get_error_code)
        3    0.000    0.000    0.000    0.000 app.py:193(_libvirt_error_handler)


['0.0047', '0.0003', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002']

  • 1st: 0.0047
  • Max: 0.0047
  • Min: 0.0002
  • Avg: 0.0007
  • Med: 0.0002

pyinstrument

% PYTHONPATH=~/qubes-core-admin-orig python3 -c 'import stats; stats.fl(count=10, profiler="pyinstrument")'
pyinstrument


  _     ._   __/__   _ _  _  _ _/_   Recorded: 08:32:27  Samples:  5
 /_//_/// /_\ / //_// / //_'/ //     Duration: 0.006     CPU time: 0.004
/   _/                      v4.6.2

Program: -c

0.005 fl  stats.py:19
└─ 0.005 f  stats.py:12
   └─ 0.005 get_vm_stats  qubes/app.py:379
      ├─ 0.003 wrapper  qubes/app.py:148
      │  └─ 0.003 lookupByID  libvirt.py:5025
      │     ├─ 0.002 virDomainLookupByID  <built-in>
      │     └─ 0.001 [self]  libvirt.py
      └─ 0.002 xc.domain_getinfo  <built-in>


['0.0038', '0.0003', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002', '0.0002']

  • 1st: 0.0038
  • Max: 0.0038
  • Min: 0.0002
  • Avg: 0.0006
  • Med: 0.0002

With my PR

% PYTHONPATH=~/qubes-core-admin python3 -c 'import stats; stats.fl(count=10, profiler="")'

['0.0044', '0.0010', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0009', '0.0008', '0.0008']

  • 1st: 0.0044
  • Max: 0.0044
  • Min: 0.0008
  • Avg: 0.0012
  • Med: 0.0009

cProfile

% PYTHONPATH=~/qubes-core-admin python3 -c 'import stats; stats.fl(count=10, profiler="cProfile")'
cProfile

         2528 function calls in 0.017 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       10    0.000    0.000    0.017    0.002 stats.py:12(f)
       10    0.001    0.000    0.016    0.002 app.py:392(get_vm_stats)
      167    0.012    0.000    0.012    0.000 {method 'read' of 'xen.lowlevel.xs.xs' objects}
      270    0.000    0.000    0.010    0.000 app.py:588(query_xs_memory)
       10    0.002    0.000    0.002    0.000 {method 'domain_getinfo' of 'xen.lowlevel.xc.xc' objects}
       18    0.000    0.000    0.002    0.000 app.py:2188(get_name_from_domid)
       14    0.000    0.000    0.001    0.000 app.py:579(query_stubdom_xid)
      388    0.000    0.000    0.000    0.000 app.py:241(is_xen)
      162    0.000    0.000    0.000    0.000 app.py:568(calculate_cpu_usage_raw)
      167    0.000    0.000    0.000    0.000 app.py:246(xs)
       10    0.000    0.000    0.000    0.000 {built-in method builtins.sorted}
      324    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
      162    0.000    0.000    0.000    0.000 {built-in method builtins.round}
       28    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
      388    0.000    0.000    0.000    0.000 app.py:196(init_vmm_connection)
      123    0.000    0.000    0.000    0.000 {method 'isdigit' of 'bytes' objects}
      180    0.000    0.000    0.000    0.000 app.py:608(<lambda>)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       10    0.000    0.000    0.000    0.000 app.py:258(xc)
       18    0.000    0.000    0.000    0.000 {method 'decode' of 'bytes' objects}
       20    0.000    0.000    0.000    0.000 {built-in method time.perf_counter_ns}
       18    0.000    0.000    0.000    0.000 {method 'endswith' of 'str' objects}
       10    0.000    0.000    0.000    0.000 {built-in method time.time}
       10    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
       10    0.000    0.000    0.000    0.000 {method 'keys' of 'dict' objects}


['0.0060', '0.0013', '0.0012', '0.0012', '0.0011', '0.0011', '0.0011', '0.0011', '0.0012', '0.0012']

  • 1st: 0.0060
  • Max: 0.0060
  • Min: 0.0011
  • Avg: 0.0016
  • Med: 0.0012

pyinstrument

% PYTHONPATH=~/qubes-core-admin python3 -c 'import stats; stats.fl(count=10, profiler="pyinstrument")'
pyinstrument


  _     ._   __/__   _ _  _  _ _/_   Recorded: 08:32:51  Samples:  14
 /_//_/// /_\ / //_// / //_'/ //     Duration: 0.015     CPU time: 0.009
/   _/                      v4.6.2

Program: -c

0.014 fl  stats.py:19
└─ 0.014 f  stats.py:12
   ├─ 0.013 get_vm_stats  qubes/app.py:392
   │  ├─ 0.007 query_xs_memory  qubes/app.py:588
   │  │  ├─ 0.006 xs.read  <built-in>
   │  │  └─ 0.001 [self]  qubes/app.py
   │  ├─ 0.002 query_stubdom_xid  qubes/app.py:579
   │  │  └─ 0.002 xs.read  <built-in>
   │  ├─ 0.002 xc.domain_getinfo  <built-in>
   │  └─ 0.002 [self]  qubes/app.py
   └─ 0.001 [self]  stats.py


['0.0048', '0.0012', '0.0010', '0.0012', '0.0012', '0.0011', '0.0011', '0.0011', '0.0011', '0.0011']

  • 1st: 0.0048
  • Max: 0.0048
  • Min: 0.0010
  • Avg: 0.0015
  • Med: 0.0011

Now just one iteration to see the cache creation.

1 iteration

Before my PR, cProfile

% PYTHONPATH=~/qubes-core-admin-orig python3 -c 'import stats; stats.fl(count=1, profiler="cProfile")'

cProfile

         1154 function calls in 0.005 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.005    0.005 stats.py:12(f)
        1    0.000    0.000    0.005    0.005 app.py:379(get_vm_stats)
       20    0.000    0.000    0.003    0.000 app.py:148(wrapper)
       20    0.000    0.000    0.003    0.000 libvirt.py:5025(lookupByID)
       20    0.003    0.000    0.003    0.000 {built-in method libvirtmod.virDomainLookupByID}
        1    0.000    0.000    0.000    0.000 {method 'domain_getinfo' of 'xen.lowlevel.xc.xc' objects}
       20    0.000    0.000    0.000    0.000 app.py:141(__getattr__)
       36    0.000    0.000    0.000    0.000 functools.py:36(update_wrapper)
       16    0.000    0.000    0.000    0.000 app.py:99(__getattr__)
       16    0.000    0.000    0.000    0.000 libvirt.py:541(__del__)
       16    0.000    0.000    0.000    0.000 {built-in method libvirtmod.virDomainFree}
      340    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
       52    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
       16    0.000    0.000    0.000    0.000 app.py:104(wrapper)
      216    0.000    0.000    0.000    0.000 {built-in method builtins.setattr}
       36    0.000    0.000    0.000    0.000 <frozen abc>:117(__instancecheck__)
       16    0.000    0.000    0.000    0.000 libvirt.py:2197(name)
       36    0.000    0.000    0.000    0.000 functools.py:66(wraps)
       16    0.000    0.000    0.000    0.000 app.py:136(_wrap_domain)
       40    0.000    0.000    0.000    0.000 {built-in method builtins.round}
       16    0.000    0.000    0.000    0.000 libvirt.py:535(__init__)
       36    0.000    0.000    0.000    0.000 {built-in method _abc._abc_instancecheck}
       16    0.000    0.000    0.000    0.000 {built-in method libvirtmod.virDomainGetName}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        4    0.000    0.000    0.000    0.000 libvirt.py:45(__init__)
        4    0.000    0.000    0.000    0.000 app.py:125(_reconnect_if_dead)
       36    0.000    0.000    0.000    0.000 {method 'update' of 'dict' objects}
       20    0.000    0.000    0.000    0.000 app.py:223(libvirt_conn)
        4    0.000    0.000    0.000    0.000 libvirt.py:4896(isAlive)
       20    0.000    0.000    0.000    0.000 {built-in method builtins.max}
        4    0.000    0.000    0.000    0.000 {built-in method libvirtmod.virConnectIsAlive}
        1    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
        2    0.000    0.000    0.000    0.000 app.py:241(is_xen)
       22    0.000    0.000    0.000    0.000 app.py:196(init_vmm_connection)
        4    0.000    0.000    0.000    0.000 {built-in method libvirtmod.virGetLastError}
        1    0.000    0.000    0.000    0.000 app.py:258(xc)
       16    0.000    0.000    0.000    0.000 app.py:76(__init__)
        4    0.000    0.000    0.000    0.000 libvirt.py:59(get_error_code)
        2    0.000    0.000    0.000    0.000 {built-in method time.perf_counter_ns}
        4    0.000    0.000    0.000    0.000 app.py:193(_libvirt_error_handler)
        1    0.000    0.000    0.000    0.000 {built-in method time.time}
        1    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}


['0.0048']

  • 1st: 0.0048
  • Max: 0.0048
  • Min: 0.0048
  • Avg: 0.0048
  • Med: 0.0048

With my PR, cProfile

% PYTHONPATH=~/qubes-core-admin python3 -c 'import stats; stats.fl(count=1, profiler="cProfile")'

cProfile

         571 function calls in 0.005 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.005    0.005 stats.py:12(f)
        1    0.000    0.000    0.005    0.005 app.py:392(get_vm_stats)
       63    0.004    0.000    0.004    0.000 {method 'read' of 'xen.lowlevel.xs.xs' objects}
       28    0.000    0.000    0.002    0.000 app.py:588(query_xs_memory)
       20    0.000    0.000    0.001    0.000 app.py:2188(get_name_from_domid)
       15    0.000    0.000    0.001    0.000 app.py:579(query_stubdom_xid)
        1    0.000    0.000    0.000    0.000 {method 'domain_getinfo' of 'xen.lowlevel.xc.xc' objects}
      137    0.000    0.000    0.000    0.000 app.py:241(is_xen)
       21    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
       63    0.000    0.000    0.000    0.000 app.py:246(xs)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.sorted}
      137    0.000    0.000    0.000    0.000 app.py:196(init_vmm_connection)
       20    0.000    0.000    0.000    0.000 {method 'decode' of 'bytes' objects}
       20    0.000    0.000    0.000    0.000 {method 'endswith' of 'str' objects}
       16    0.000    0.000    0.000    0.000 {method 'isdigit' of 'bytes' objects}
       20    0.000    0.000    0.000    0.000 app.py:608(<lambda>)
        1    0.000    0.000    0.000    0.000 app.py:258(xc)
        2    0.000    0.000    0.000    0.000 {built-in method time.perf_counter_ns}
        1    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {built-in method time.time}
        1    0.000    0.000    0.000    0.000 {method 'keys' of 'dict' objects}


['0.0050']

  • 1st: 0.0050
  • Max: 0.0050
  • Min: 0.0050
  • Avg: 0.0050
  • Med: 0.0050

  • Expensive calls before my PR:
        1    0.000    0.000    0.005    0.005 app.py:379(get_vm_stats)
       20    0.000    0.000    0.003    0.000 app.py:148(wrapper)
       20    0.000    0.000    0.003    0.000 libvirt.py:5025(lookupByID)
       20    0.003    0.000    0.003    0.000 {built-in method libvirtmod.virDomainLookupByID}
  • Expensive calls after my PR:
        1    0.000    0.000    0.005    0.005 app.py:392(get_vm_stats)
       63    0.004    0.000    0.004    0.000 {method 'read' of 'xen.lowlevel.xs.xs' objects}
       28    0.000    0.000    0.002    0.000 app.py:588(query_xs_memory)
       20    0.000    0.000    0.001    0.000 app.py:2188(get_name_from_domid)
       15    0.000    0.000    0.001    0.000 app.py:579(query_stubdom_xid)

There is not much I can shave here from the first call.

63 xs.read:

  • 28 query_xs_memory (optimized)
  • 20 get_name_from_domid (optimized)
  • 15 query_stubdom_xid (NOT optimized)

Hmm, there should only be 4 calls to query_stubdom_xid, one for every HVM, so there is 11 extra xs.read calls here.

So we are spending 0.975ms on this:

>>> "{:.6f}".format(0.000065 * 15)
'0.000975'
And we should be spending only `0.26`ms.

"{:.6f}".format(0.000065 * 4)
'0.000260'


But why spending more time than necessary, a call count that increases depending on the amount if there are more domains running?

```python3
>>> info = app.vmm.xc.domain_getinfo(0)
>>> new = {i['domid']: {'name': app.vmm.xs.read("", f"/local/domain/{i['domid']}/name"), 'hvm': i['hvm']} for i in info}
>>> for k, v in new.items():
...     print(k, v)
...
0 {'name': b'Domain-0', 'hvm': 0}
4 {'name': b'sys-net', 'hvm': 1}
5 {'name': b'sys-net-dm', 'hvm': 0}
6 {'name': b'sys-firewall', 'hvm': 1}
83 {'name': b'openbsd-79', 'hvm': 1}
84 {'name': b'openbsd-79-dm', 'hvm': 0}
85 {'name': b'ubuntu-26.04', 'hvm': 1}
86 {'name': b'ubuntu-26.04-dm', 'hvm': 0}
87 {'name': b'archlinux-dvm', 'hvm': 1}
88 {'name': b'test', 'hvm': 1}
89 {'name': b'default-dvm-debian', 'hvm': 1}
90 {'name': b'default-mgmt-dvm-debian', 'hvm': 1}
91 {'name': b'test1234-dvm', 'hvm': 1}
92 {'name': b'default-dvm-minimal', 'hvm': 1}
93 {'name': b'default-mgmt-dvm', 'hvm': 1}
94 {'name': b'test-dvm', 'hvm': 1}
95 {'name': b'default-dvm-gui-test', 'hvm': 1}
96 {'name': b'ttt-dvm', 'hvm': 1}
97 {'name': b'windows-11', 'hvm': 1}
98 {'name': b'windows-11-dm', 'hvm': 0}
>>>
>>> for qube in app.domains:
...     if qube.is_running():
...         print(qube.name, getattr(qube, "virt_mode", None))
...
dom0 None
archlinux-dvm pvh
default-dvm-debian pvh
default-dvm-gui-test pvh
default-dvm-minimal pvh
default-mgmt-dvm pvh
default-mgmt-dvm-debian pvh
openbsd-79 hvm
sys-firewall pvh
sys-net hvm
test pvh
test-dvm pvh
test1234-dvm pvh
ttt-dvm pvh
ubuntu-26.04 hvm
windows-11 hvm
>>>
>>> for qube in app.domains:
...     if qube.is_running() and getattr(qube, "virt_mode", None) == "hvm":
...         print(qube.name, "hvm")
...
openbsd-79 hvm
sys-net hvm
ubuntu-26.04 hvm
windows-11 hvm

xs.domain_getinfo doesn't report hvm: bool if qube is HVM or not. For our case, It returns 0 if "dom0" or "stubdomain", the rest is always 1, which of the many domains, some are HVM.

https://github.com/xen-project/xen/blob/99794c8a8ff8b1d277c09d4736384fd5bb94f2d6/tools/python/xen/lowlevel/xc/xc.c#L377

https://github.com/xen-project/xen/blob/99794c8a8ff8b1d277c09d4736384fd5bb94f2d6/xen/include/public/domctl.h#L143

https://github.com/xen-project/xen/blob/99794c8a8ff8b1d277c09d4736384fd5bb94f2d6/xen/common/domctl.c#L91

https://github.com/xen-project/xen/blob/99794c8a8ff8b1d277c09d4736384fd5bb94f2d6/xen/include/xen/sched.h#L1218

https://github.com/QubesOS/xen/blob/1ed7a7ebae8541937b934e917bc1f674d013b4da/xen/include/public/domctl.h#L49

Is upstream right? If not, new entry on domain_getinfo to avoid breaking some code that depends on that hvm value? Would be nice to also include domain name, but there may be bigger reasons to not include it that I don't know.

In contrast. libvirt_domain.OSType() shows hvm correctly as we control it with templates/libvirt/xen.xml.

I think I can hack the query of HVM a bit, when creating cache of a domain:

First loop:

  • cache names first
  • count names ending with -dm is the same number as HVMs

Second loop:

  • query stubdom_xid until we found enough of them

The downside is that if HVMs are the last domain started, this doesn't optimize anything at all.

@ben-grande
ben-grande force-pushed the stat-cpu branch 2 times, most recently from 422ab61 to 6a89e94 Compare July 23, 2026 08:52
@marmarek

Copy link
Copy Markdown
Member

xs.domain_getinfo doesn't report hvm: bool if qube is HVM or not. For our case, It returns 0 if "dom0" or "stubdomain", the rest is always 1, which of the many domains, some are HVM.

As far as hypervisor is concerned, HVM and PVH are literally the same. PVH is simply HVM without device model (QEMU)...

@marmarek

Copy link
Copy Markdown
Member

In a recent push you reverted back to the memory_assigned_max name, why?

@ben-grande

Copy link
Copy Markdown
Contributor Author

In a recent push you reverted back to the memory_assigned_max name, why?

I ended up agreeing with you. I think it is a better name than memory_assigned_with_overhead.

@ben-grande
ben-grande marked this pull request as ready for review July 23, 2026 13:44
@marmarek

Copy link
Copy Markdown
Member

I ended up agreeing with you. I think it is a better name than memory_assigned_with_overhead.

But I actually requested to not use "max" for this value - #827 (comment)

@ben-grande

ben-grande commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

How.... did my memory fail, ok, will use memory_assigned_with_overhead again, or memory_assigned_total.

- Only meminfo reports the correct memory used for each balanced domain
- Including overhead can be used to calculate how much memory is
  reserved for all qubes
- Excluding overhead, just memory available in the qube on
  memory-balanced qubes, indicates memory that it can possibly get.

For: QubesOS/qubes-issues#8368
Although both values can be lied by the qube, we can distinguish the
"memory types" in client tools.

For: QubesOS/qubes-linux-utils#140
It's not completely Xen agnostic because then it would not be possible
to query stubdomains, as libvirt is not aware of them. On the other
hand, non-Xen has one more API methods working.

When not using Xen, construct the dictionary as "xc.domain_getinfo()"
would, so the info loop can consume from both inputs.

Stubdomains are returned as separate keys instead of aggregated to allow
clients to distinguish if it's HVM using the resources or it's
stubdomain. This is specially relevant for HVMs that have memory
balancing enabled.
Changing maxmem property of a running domain allows qmemman to read new
information and balance appropriately. For the maxmem property to
accurately reflect current state for clients to query, it is forbidden
to change it for a running non memory balanced domain.
Add "maxmem" property to "Qubes" (app) and "AdminVM", so clients can now
system/host memory and dom0 memory.

Fix "get_mem" and "get_mem_static_max" mess:

- AdminVM:
  - "get_mem": supposedly, should report usage in KiB, but it's reading
    /proc/meminfo and returning kB
  - "get_mem_static_max": returning total host memory
- QubesVM:
  - "get_mem": supposedly, should report usage, but it's
    reporting initial memory
  - "get_mem_static_max": same as above

Now all of these methods returns memory in KiB. "get_mem" returns
allocated/assigned memory, while "get_mem_static_max" returns maxmem in
KiB.
Breaking change. No known Qubes OS code uses. A better value is
"cpu_usage".
Xenstore keys are created immediately after domain is crated and before
tasks can be switched with "await", to guarantee that qubesd methods
(same thread) don't fail attempts to read xenstore keys. This is the
case for "Qubes.get_vm_stats", which interacts with VMM methods instead
of qubes events.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants