1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888 |
- #!/usr/bin/env python3
- # SPDX-License-Identifier: GPL-2.0-only
- #
- # top-like utility for displaying kvm statistics
- #
- # Copyright 2006-2008 Qumranet Technologies
- # Copyright 2008-2011 Red Hat, Inc.
- #
- # Authors:
- # Avi Kivity <[email protected]>
- #
- """The kvm_stat module outputs statistics about running KVM VMs
- Three different ways of output formatting are available:
- - as a top-like text ui
- - in a key -> value format
- - in an all keys, all values format
- The data is sampled from the KVM's debugfs entries and its perf events.
- """
- from __future__ import print_function
- import curses
- import sys
- import locale
- import os
- import time
- import argparse
- import ctypes
- import fcntl
- import resource
- import struct
- import re
- import subprocess
- import signal
- from collections import defaultdict, namedtuple
- from functools import reduce
- from datetime import datetime
- VMX_EXIT_REASONS = {
- 'EXCEPTION_NMI': 0,
- 'EXTERNAL_INTERRUPT': 1,
- 'TRIPLE_FAULT': 2,
- 'INIT_SIGNAL': 3,
- 'SIPI_SIGNAL': 4,
- 'INTERRUPT_WINDOW': 7,
- 'NMI_WINDOW': 8,
- 'TASK_SWITCH': 9,
- 'CPUID': 10,
- 'HLT': 12,
- 'INVD': 13,
- 'INVLPG': 14,
- 'RDPMC': 15,
- 'RDTSC': 16,
- 'VMCALL': 18,
- 'VMCLEAR': 19,
- 'VMLAUNCH': 20,
- 'VMPTRLD': 21,
- 'VMPTRST': 22,
- 'VMREAD': 23,
- 'VMRESUME': 24,
- 'VMWRITE': 25,
- 'VMOFF': 26,
- 'VMON': 27,
- 'CR_ACCESS': 28,
- 'DR_ACCESS': 29,
- 'IO_INSTRUCTION': 30,
- 'MSR_READ': 31,
- 'MSR_WRITE': 32,
- 'INVALID_STATE': 33,
- 'MSR_LOAD_FAIL': 34,
- 'MWAIT_INSTRUCTION': 36,
- 'MONITOR_TRAP_FLAG': 37,
- 'MONITOR_INSTRUCTION': 39,
- 'PAUSE_INSTRUCTION': 40,
- 'MCE_DURING_VMENTRY': 41,
- 'TPR_BELOW_THRESHOLD': 43,
- 'APIC_ACCESS': 44,
- 'EOI_INDUCED': 45,
- 'GDTR_IDTR': 46,
- 'LDTR_TR': 47,
- 'EPT_VIOLATION': 48,
- 'EPT_MISCONFIG': 49,
- 'INVEPT': 50,
- 'RDTSCP': 51,
- 'PREEMPTION_TIMER': 52,
- 'INVVPID': 53,
- 'WBINVD': 54,
- 'XSETBV': 55,
- 'APIC_WRITE': 56,
- 'RDRAND': 57,
- 'INVPCID': 58,
- 'VMFUNC': 59,
- 'ENCLS': 60,
- 'RDSEED': 61,
- 'PML_FULL': 62,
- 'XSAVES': 63,
- 'XRSTORS': 64,
- 'UMWAIT': 67,
- 'TPAUSE': 68,
- 'BUS_LOCK': 74,
- 'NOTIFY': 75,
- }
- SVM_EXIT_REASONS = {
- 'READ_CR0': 0x000,
- 'READ_CR2': 0x002,
- 'READ_CR3': 0x003,
- 'READ_CR4': 0x004,
- 'READ_CR8': 0x008,
- 'WRITE_CR0': 0x010,
- 'WRITE_CR2': 0x012,
- 'WRITE_CR3': 0x013,
- 'WRITE_CR4': 0x014,
- 'WRITE_CR8': 0x018,
- 'READ_DR0': 0x020,
- 'READ_DR1': 0x021,
- 'READ_DR2': 0x022,
- 'READ_DR3': 0x023,
- 'READ_DR4': 0x024,
- 'READ_DR5': 0x025,
- 'READ_DR6': 0x026,
- 'READ_DR7': 0x027,
- 'WRITE_DR0': 0x030,
- 'WRITE_DR1': 0x031,
- 'WRITE_DR2': 0x032,
- 'WRITE_DR3': 0x033,
- 'WRITE_DR4': 0x034,
- 'WRITE_DR5': 0x035,
- 'WRITE_DR6': 0x036,
- 'WRITE_DR7': 0x037,
- 'EXCP_BASE': 0x040,
- 'LAST_EXCP': 0x05f,
- 'INTR': 0x060,
- 'NMI': 0x061,
- 'SMI': 0x062,
- 'INIT': 0x063,
- 'VINTR': 0x064,
- 'CR0_SEL_WRITE': 0x065,
- 'IDTR_READ': 0x066,
- 'GDTR_READ': 0x067,
- 'LDTR_READ': 0x068,
- 'TR_READ': 0x069,
- 'IDTR_WRITE': 0x06a,
- 'GDTR_WRITE': 0x06b,
- 'LDTR_WRITE': 0x06c,
- 'TR_WRITE': 0x06d,
- 'RDTSC': 0x06e,
- 'RDPMC': 0x06f,
- 'PUSHF': 0x070,
- 'POPF': 0x071,
- 'CPUID': 0x072,
- 'RSM': 0x073,
- 'IRET': 0x074,
- 'SWINT': 0x075,
- 'INVD': 0x076,
- 'PAUSE': 0x077,
- 'HLT': 0x078,
- 'INVLPG': 0x079,
- 'INVLPGA': 0x07a,
- 'IOIO': 0x07b,
- 'MSR': 0x07c,
- 'TASK_SWITCH': 0x07d,
- 'FERR_FREEZE': 0x07e,
- 'SHUTDOWN': 0x07f,
- 'VMRUN': 0x080,
- 'VMMCALL': 0x081,
- 'VMLOAD': 0x082,
- 'VMSAVE': 0x083,
- 'STGI': 0x084,
- 'CLGI': 0x085,
- 'SKINIT': 0x086,
- 'RDTSCP': 0x087,
- 'ICEBP': 0x088,
- 'WBINVD': 0x089,
- 'MONITOR': 0x08a,
- 'MWAIT': 0x08b,
- 'MWAIT_COND': 0x08c,
- 'XSETBV': 0x08d,
- 'RDPRU': 0x08e,
- 'EFER_WRITE_TRAP': 0x08f,
- 'CR0_WRITE_TRAP': 0x090,
- 'CR1_WRITE_TRAP': 0x091,
- 'CR2_WRITE_TRAP': 0x092,
- 'CR3_WRITE_TRAP': 0x093,
- 'CR4_WRITE_TRAP': 0x094,
- 'CR5_WRITE_TRAP': 0x095,
- 'CR6_WRITE_TRAP': 0x096,
- 'CR7_WRITE_TRAP': 0x097,
- 'CR8_WRITE_TRAP': 0x098,
- 'CR9_WRITE_TRAP': 0x099,
- 'CR10_WRITE_TRAP': 0x09a,
- 'CR11_WRITE_TRAP': 0x09b,
- 'CR12_WRITE_TRAP': 0x09c,
- 'CR13_WRITE_TRAP': 0x09d,
- 'CR14_WRITE_TRAP': 0x09e,
- 'CR15_WRITE_TRAP': 0x09f,
- 'INVPCID': 0x0a2,
- 'NPF': 0x400,
- 'AVIC_INCOMPLETE_IPI': 0x401,
- 'AVIC_UNACCELERATED_ACCESS': 0x402,
- 'VMGEXIT': 0x403,
- }
- # EC definition of HSR (from arch/arm64/include/asm/esr.h)
- AARCH64_EXIT_REASONS = {
- 'UNKNOWN': 0x00,
- 'WFx': 0x01,
- 'CP15_32': 0x03,
- 'CP15_64': 0x04,
- 'CP14_MR': 0x05,
- 'CP14_LS': 0x06,
- 'FP_ASIMD': 0x07,
- 'CP10_ID': 0x08,
- 'PAC': 0x09,
- 'CP14_64': 0x0C,
- 'BTI': 0x0D,
- 'ILL': 0x0E,
- 'SVC32': 0x11,
- 'HVC32': 0x12,
- 'SMC32': 0x13,
- 'SVC64': 0x15,
- 'HVC64': 0x16,
- 'SMC64': 0x17,
- 'SYS64': 0x18,
- 'SVE': 0x19,
- 'ERET': 0x1A,
- 'FPAC': 0x1C,
- 'SME': 0x1D,
- 'IMP_DEF': 0x1F,
- 'IABT_LOW': 0x20,
- 'IABT_CUR': 0x21,
- 'PC_ALIGN': 0x22,
- 'DABT_LOW': 0x24,
- 'DABT_CUR': 0x25,
- 'SP_ALIGN': 0x26,
- 'FP_EXC32': 0x28,
- 'FP_EXC64': 0x2C,
- 'SERROR': 0x2F,
- 'BREAKPT_LOW': 0x30,
- 'BREAKPT_CUR': 0x31,
- 'SOFTSTP_LOW': 0x32,
- 'SOFTSTP_CUR': 0x33,
- 'WATCHPT_LOW': 0x34,
- 'WATCHPT_CUR': 0x35,
- 'BKPT32': 0x38,
- 'VECTOR32': 0x3A,
- 'BRK64': 0x3C,
- }
- # From include/uapi/linux/kvm.h, KVM_EXIT_xxx
- USERSPACE_EXIT_REASONS = {
- 'UNKNOWN': 0,
- 'EXCEPTION': 1,
- 'IO': 2,
- 'HYPERCALL': 3,
- 'DEBUG': 4,
- 'HLT': 5,
- 'MMIO': 6,
- 'IRQ_WINDOW_OPEN': 7,
- 'SHUTDOWN': 8,
- 'FAIL_ENTRY': 9,
- 'INTR': 10,
- 'SET_TPR': 11,
- 'TPR_ACCESS': 12,
- 'S390_SIEIC': 13,
- 'S390_RESET': 14,
- 'DCR': 15,
- 'NMI': 16,
- 'INTERNAL_ERROR': 17,
- 'OSI': 18,
- 'PAPR_HCALL': 19,
- 'S390_UCONTROL': 20,
- 'WATCHDOG': 21,
- 'S390_TSCH': 22,
- 'EPR': 23,
- 'SYSTEM_EVENT': 24,
- 'S390_STSI': 25,
- 'IOAPIC_EOI': 26,
- 'HYPERV': 27,
- 'ARM_NISV': 28,
- 'X86_RDMSR': 29,
- 'X86_WRMSR': 30,
- 'DIRTY_RING_FULL': 31,
- 'AP_RESET_HOLD': 32,
- 'X86_BUS_LOCK': 33,
- 'XEN': 34,
- 'RISCV_SBI': 35,
- 'RISCV_CSR': 36,
- 'NOTIFY': 37,
- }
- IOCTL_NUMBERS = {
- 'SET_FILTER': 0x40082406,
- 'ENABLE': 0x00002400,
- 'DISABLE': 0x00002401,
- 'RESET': 0x00002403,
- }
- signal_received = False
- ENCODING = locale.getpreferredencoding(False)
- TRACE_FILTER = re.compile(r'^[^\(]*$')
- class Arch(object):
- """Encapsulates global architecture specific data.
- Contains the performance event open syscall and ioctl numbers, as
- well as the VM exit reasons for the architecture it runs on.
- """
- @staticmethod
- def get_arch():
- machine = os.uname()[4]
- if machine.startswith('ppc'):
- return ArchPPC()
- elif machine.startswith('aarch64'):
- return ArchA64()
- elif machine.startswith('s390'):
- return ArchS390()
- else:
- # X86_64
- for line in open('/proc/cpuinfo'):
- if not line.startswith('flags'):
- continue
- flags = line.split()
- if 'vmx' in flags:
- return ArchX86(VMX_EXIT_REASONS)
- if 'svm' in flags:
- return ArchX86(SVM_EXIT_REASONS)
- return
- def tracepoint_is_child(self, field):
- if (TRACE_FILTER.match(field)):
- return None
- return field.split('(', 1)[0]
- class ArchX86(Arch):
- def __init__(self, exit_reasons):
- self.sc_perf_evt_open = 298
- self.ioctl_numbers = IOCTL_NUMBERS
- self.exit_reason_field = 'exit_reason'
- self.exit_reasons = exit_reasons
- def debugfs_is_child(self, field):
- """ Returns name of parent if 'field' is a child, None otherwise """
- return None
- class ArchPPC(Arch):
- def __init__(self):
- self.sc_perf_evt_open = 319
- self.ioctl_numbers = IOCTL_NUMBERS
- self.ioctl_numbers['ENABLE'] = 0x20002400
- self.ioctl_numbers['DISABLE'] = 0x20002401
- self.ioctl_numbers['RESET'] = 0x20002403
- # PPC comes in 32 and 64 bit and some generated ioctl
- # numbers depend on the wordsize.
- char_ptr_size = ctypes.sizeof(ctypes.c_char_p)
- self.ioctl_numbers['SET_FILTER'] = 0x80002406 | char_ptr_size << 16
- self.exit_reason_field = 'exit_nr'
- self.exit_reasons = {}
- def debugfs_is_child(self, field):
- """ Returns name of parent if 'field' is a child, None otherwise """
- return None
- class ArchA64(Arch):
- def __init__(self):
- self.sc_perf_evt_open = 241
- self.ioctl_numbers = IOCTL_NUMBERS
- self.exit_reason_field = 'esr_ec'
- self.exit_reasons = AARCH64_EXIT_REASONS
- def debugfs_is_child(self, field):
- """ Returns name of parent if 'field' is a child, None otherwise """
- return None
- class ArchS390(Arch):
- def __init__(self):
- self.sc_perf_evt_open = 331
- self.ioctl_numbers = IOCTL_NUMBERS
- self.exit_reason_field = None
- self.exit_reasons = None
- def debugfs_is_child(self, field):
- """ Returns name of parent if 'field' is a child, None otherwise """
- if field.startswith('instruction_'):
- return 'exit_instruction'
- ARCH = Arch.get_arch()
- class perf_event_attr(ctypes.Structure):
- """Struct that holds the necessary data to set up a trace event.
- For an extensive explanation see perf_event_open(2) and
- include/uapi/linux/perf_event.h, struct perf_event_attr
- All fields that are not initialized in the constructor are 0.
- """
- _fields_ = [('type', ctypes.c_uint32),
- ('size', ctypes.c_uint32),
- ('config', ctypes.c_uint64),
- ('sample_freq', ctypes.c_uint64),
- ('sample_type', ctypes.c_uint64),
- ('read_format', ctypes.c_uint64),
- ('flags', ctypes.c_uint64),
- ('wakeup_events', ctypes.c_uint32),
- ('bp_type', ctypes.c_uint32),
- ('bp_addr', ctypes.c_uint64),
- ('bp_len', ctypes.c_uint64),
- ]
- def __init__(self):
- super(self.__class__, self).__init__()
- self.type = PERF_TYPE_TRACEPOINT
- self.size = ctypes.sizeof(self)
- self.read_format = PERF_FORMAT_GROUP
- PERF_TYPE_TRACEPOINT = 2
- PERF_FORMAT_GROUP = 1 << 3
- class Group(object):
- """Represents a perf event group."""
- def __init__(self):
- self.events = []
- def add_event(self, event):
- self.events.append(event)
- def read(self):
- """Returns a dict with 'event name: value' for all events in the
- group.
- Values are read by reading from the file descriptor of the
- event that is the group leader. See perf_event_open(2) for
- details.
- Read format for the used event configuration is:
- struct read_format {
- u64 nr; /* The number of events */
- struct {
- u64 value; /* The value of the event */
- } values[nr];
- };
- """
- length = 8 * (1 + len(self.events))
- read_format = 'xxxxxxxx' + 'Q' * len(self.events)
- return dict(zip([event.name for event in self.events],
- struct.unpack(read_format,
- os.read(self.events[0].fd, length))))
- class Event(object):
- """Represents a performance event and manages its life cycle."""
- def __init__(self, name, group, trace_cpu, trace_pid, trace_point,
- trace_filter, trace_set='kvm'):
- self.libc = ctypes.CDLL('libc.so.6', use_errno=True)
- self.syscall = self.libc.syscall
- self.name = name
- self.fd = None
- self._setup_event(group, trace_cpu, trace_pid, trace_point,
- trace_filter, trace_set)
- def __del__(self):
- """Closes the event's file descriptor.
- As no python file object was created for the file descriptor,
- python will not reference count the descriptor and will not
- close it itself automatically, so we do it.
- """
- if self.fd:
- os.close(self.fd)
- def _perf_event_open(self, attr, pid, cpu, group_fd, flags):
- """Wrapper for the sys_perf_evt_open() syscall.
- Used to set up performance events, returns a file descriptor or -1
- on error.
- Attributes are:
- - syscall number
- - struct perf_event_attr *
- - pid or -1 to monitor all pids
- - cpu number or -1 to monitor all cpus
- - The file descriptor of the group leader or -1 to create a group.
- - flags
- """
- return self.syscall(ARCH.sc_perf_evt_open, ctypes.pointer(attr),
- ctypes.c_int(pid), ctypes.c_int(cpu),
- ctypes.c_int(group_fd), ctypes.c_long(flags))
- def _setup_event_attribute(self, trace_set, trace_point):
- """Returns an initialized ctype perf_event_attr struct."""
- id_path = os.path.join(PATH_DEBUGFS_TRACING, 'events', trace_set,
- trace_point, 'id')
- event_attr = perf_event_attr()
- event_attr.config = int(open(id_path).read())
- return event_attr
- def _setup_event(self, group, trace_cpu, trace_pid, trace_point,
- trace_filter, trace_set):
- """Sets up the perf event in Linux.
- Issues the syscall to register the event in the kernel and
- then sets the optional filter.
- """
- event_attr = self._setup_event_attribute(trace_set, trace_point)
- # First event will be group leader.
- group_leader = -1
- # All others have to pass the leader's descriptor instead.
- if group.events:
- group_leader = group.events[0].fd
- fd = self._perf_event_open(event_attr, trace_pid,
- trace_cpu, group_leader, 0)
- if fd == -1:
- err = ctypes.get_errno()
- raise OSError(err, os.strerror(err),
- 'while calling sys_perf_event_open().')
- if trace_filter:
- fcntl.ioctl(fd, ARCH.ioctl_numbers['SET_FILTER'],
- trace_filter)
- self.fd = fd
- def enable(self):
- """Enables the trace event in the kernel.
- Enabling the group leader makes reading counters from it and the
- events under it possible.
- """
- fcntl.ioctl(self.fd, ARCH.ioctl_numbers['ENABLE'], 0)
- def disable(self):
- """Disables the trace event in the kernel.
- Disabling the group leader makes reading all counters under it
- impossible.
- """
- fcntl.ioctl(self.fd, ARCH.ioctl_numbers['DISABLE'], 0)
- def reset(self):
- """Resets the count of the trace event in the kernel."""
- fcntl.ioctl(self.fd, ARCH.ioctl_numbers['RESET'], 0)
- class Provider(object):
- """Encapsulates functionalities used by all providers."""
- def __init__(self, pid):
- self.child_events = False
- self.pid = pid
- @staticmethod
- def is_field_wanted(fields_filter, field):
- """Indicate whether field is valid according to fields_filter."""
- if not fields_filter:
- return True
- return re.match(fields_filter, field) is not None
- @staticmethod
- def walkdir(path):
- """Returns os.walk() data for specified directory.
- As it is only a wrapper it returns the same 3-tuple of (dirpath,
- dirnames, filenames).
- """
- return next(os.walk(path))
- class TracepointProvider(Provider):
- """Data provider for the stats class.
- Manages the events/groups from which it acquires its data.
- """
- def __init__(self, pid, fields_filter):
- self.group_leaders = []
- self.filters = self._get_filters()
- self.update_fields(fields_filter)
- super(TracepointProvider, self).__init__(pid)
- @staticmethod
- def _get_filters():
- """Returns a dict of trace events, their filter ids and
- the values that can be filtered.
- Trace events can be filtered for special values by setting a
- filter string via an ioctl. The string normally has the format
- identifier==value. For each filter a new event will be created, to
- be able to distinguish the events.
- """
- filters = {}
- filters['kvm_userspace_exit'] = ('reason', USERSPACE_EXIT_REASONS)
- if ARCH.exit_reason_field and ARCH.exit_reasons:
- filters['kvm_exit'] = (ARCH.exit_reason_field, ARCH.exit_reasons)
- return filters
- def _get_available_fields(self):
- """Returns a list of available events of format 'event name(filter
- name)'.
- All available events have directories under
- /sys/kernel/debug/tracing/events/ which export information
- about the specific event. Therefore, listing the dirs gives us
- a list of all available events.
- Some events like the vm exit reasons can be filtered for
- specific values. To take account for that, the routine below
- creates special fields with the following format:
- event name(filter name)
- """
- path = os.path.join(PATH_DEBUGFS_TRACING, 'events', 'kvm')
- fields = self.walkdir(path)[1]
- extra = []
- for field in fields:
- if field in self.filters:
- filter_name_, filter_dicts = self.filters[field]
- for name in filter_dicts:
- extra.append(field + '(' + name + ')')
- fields += extra
- return fields
- def update_fields(self, fields_filter):
- """Refresh fields, applying fields_filter"""
- self.fields = [field for field in self._get_available_fields()
- if self.is_field_wanted(fields_filter, field)]
- # add parents for child fields - otherwise we won't see any output!
- for field in self._fields:
- parent = ARCH.tracepoint_is_child(field)
- if (parent and parent not in self._fields):
- self.fields.append(parent)
- @staticmethod
- def _get_online_cpus():
- """Returns a list of cpu id integers."""
- def parse_int_list(list_string):
- """Returns an int list from a string of comma separated integers and
- integer ranges."""
- integers = []
- members = list_string.split(',')
- for member in members:
- if '-' not in member:
- integers.append(int(member))
- else:
- int_range = member.split('-')
- integers.extend(range(int(int_range[0]),
- int(int_range[1]) + 1))
- return integers
- with open('/sys/devices/system/cpu/online') as cpu_list:
- cpu_string = cpu_list.readline()
- return parse_int_list(cpu_string)
- def _setup_traces(self):
- """Creates all event and group objects needed to be able to retrieve
- data."""
- fields = self._get_available_fields()
- if self._pid > 0:
- # Fetch list of all threads of the monitored pid, as qemu
- # starts a thread for each vcpu.
- path = os.path.join('/proc', str(self._pid), 'task')
- groupids = self.walkdir(path)[1]
- else:
- groupids = self._get_online_cpus()
- # The constant is needed as a buffer for python libs, std
- # streams and other files that the script opens.
- newlim = len(groupids) * len(fields) + 50
- try:
- softlim_, hardlim = resource.getrlimit(resource.RLIMIT_NOFILE)
- if hardlim < newlim:
- # Now we need CAP_SYS_RESOURCE, to increase the hard limit.
- resource.setrlimit(resource.RLIMIT_NOFILE, (newlim, newlim))
- else:
- # Raising the soft limit is sufficient.
- resource.setrlimit(resource.RLIMIT_NOFILE, (newlim, hardlim))
- except ValueError:
- sys.exit("NOFILE rlimit could not be raised to {0}".format(newlim))
- for groupid in groupids:
- group = Group()
- for name in fields:
- tracepoint = name
- tracefilter = None
- match = re.match(r'(.*)\((.*)\)', name)
- if match:
- tracepoint, sub = match.groups()
- tracefilter = ('%s==%d\0' %
- (self.filters[tracepoint][0],
- self.filters[tracepoint][1][sub]))
- # From perf_event_open(2):
- # pid > 0 and cpu == -1
- # This measures the specified process/thread on any CPU.
- #
- # pid == -1 and cpu >= 0
- # This measures all processes/threads on the specified CPU.
- trace_cpu = groupid if self._pid == 0 else -1
- trace_pid = int(groupid) if self._pid != 0 else -1
- group.add_event(Event(name=name,
- group=group,
- trace_cpu=trace_cpu,
- trace_pid=trace_pid,
- trace_point=tracepoint,
- trace_filter=tracefilter))
- self.group_leaders.append(group)
- @property
- def fields(self):
- return self._fields
- @fields.setter
- def fields(self, fields):
- """Enables/disables the (un)wanted events"""
- self._fields = fields
- for group in self.group_leaders:
- for index, event in enumerate(group.events):
- if event.name in fields:
- event.reset()
- event.enable()
- else:
- # Do not disable the group leader.
- # It would disable all of its events.
- if index != 0:
- event.disable()
- @property
- def pid(self):
- return self._pid
- @pid.setter
- def pid(self, pid):
- """Changes the monitored pid by setting new traces."""
- self._pid = pid
- # The garbage collector will get rid of all Event/Group
- # objects and open files after removing the references.
- self.group_leaders = []
- self._setup_traces()
- self.fields = self._fields
- def read(self, by_guest=0):
- """Returns 'event name: current value' for all enabled events."""
- ret = defaultdict(int)
- for group in self.group_leaders:
- for name, val in group.read().items():
- if name not in self._fields:
- continue
- parent = ARCH.tracepoint_is_child(name)
- if parent:
- name += ' ' + parent
- ret[name] += val
- return ret
- def reset(self):
- """Reset all field counters"""
- for group in self.group_leaders:
- for event in group.events:
- event.reset()
- class DebugfsProvider(Provider):
- """Provides data from the files that KVM creates in the kvm debugfs
- folder."""
- def __init__(self, pid, fields_filter, include_past):
- self.update_fields(fields_filter)
- self._baseline = {}
- self.do_read = True
- self.paths = []
- super(DebugfsProvider, self).__init__(pid)
- if include_past:
- self._restore()
- def _get_available_fields(self):
- """"Returns a list of available fields.
- The fields are all available KVM debugfs files
- """
- exempt_list = ['halt_poll_fail_ns', 'halt_poll_success_ns', 'halt_wait_ns']
- fields = [field for field in self.walkdir(PATH_DEBUGFS_KVM)[2]
- if field not in exempt_list]
- return fields
- def update_fields(self, fields_filter):
- """Refresh fields, applying fields_filter"""
- self._fields = [field for field in self._get_available_fields()
- if self.is_field_wanted(fields_filter, field)]
- # add parents for child fields - otherwise we won't see any output!
- for field in self._fields:
- parent = ARCH.debugfs_is_child(field)
- if (parent and parent not in self._fields):
- self.fields.append(parent)
- @property
- def fields(self):
- return self._fields
- @fields.setter
- def fields(self, fields):
- self._fields = fields
- self.reset()
- @property
- def pid(self):
- return self._pid
- @pid.setter
- def pid(self, pid):
- self._pid = pid
- if pid != 0:
- vms = self.walkdir(PATH_DEBUGFS_KVM)[1]
- if len(vms) == 0:
- self.do_read = False
- self.paths = list(filter(lambda x: "{}-".format(pid) in x, vms))
- else:
- self.paths = []
- self.do_read = True
- def _verify_paths(self):
- """Remove invalid paths"""
- for path in self.paths:
- if not os.path.exists(os.path.join(PATH_DEBUGFS_KVM, path)):
- self.paths.remove(path)
- continue
- def read(self, reset=0, by_guest=0):
- """Returns a dict with format:'file name / field -> current value'.
- Parameter 'reset':
- 0 plain read
- 1 reset field counts to 0
- 2 restore the original field counts
- """
- results = {}
- # If no debugfs filtering support is available, then don't read.
- if not self.do_read:
- return results
- self._verify_paths()
- paths = self.paths
- if self._pid == 0:
- paths = []
- for entry in os.walk(PATH_DEBUGFS_KVM):
- for dir in entry[1]:
- paths.append(dir)
- for path in paths:
- for field in self._fields:
- value = self._read_field(field, path)
- key = path + field
- if reset == 1:
- self._baseline[key] = value
- if reset == 2:
- self._baseline[key] = 0
- if self._baseline.get(key, -1) == -1:
- self._baseline[key] = value
- parent = ARCH.debugfs_is_child(field)
- if parent:
- field = field + ' ' + parent
- else:
- if by_guest:
- field = key.split('-')[0] # set 'field' to 'pid'
- increment = value - self._baseline.get(key, 0)
- if field in results:
- results[field] += increment
- else:
- results[field] = increment
- return results
- def _read_field(self, field, path):
- """Returns the value of a single field from a specific VM."""
- try:
- return int(open(os.path.join(PATH_DEBUGFS_KVM,
- path,
- field))
- .read())
- except IOError:
- return 0
- def reset(self):
- """Reset field counters"""
- self._baseline = {}
- self.read(1)
- def _restore(self):
- """Reset field counters"""
- self._baseline = {}
- self.read(2)
- EventStat = namedtuple('EventStat', ['value', 'delta'])
- class Stats(object):
- """Manages the data providers and the data they provide.
- It is used to set filters on the provider's data and collect all
- provider data.
- """
- def __init__(self, options):
- self.providers = self._get_providers(options)
- self._pid_filter = options.pid
- self._fields_filter = options.fields
- self.values = {}
- self._child_events = False
- def _get_providers(self, options):
- """Returns a list of data providers depending on the passed options."""
- providers = []
- if options.debugfs:
- providers.append(DebugfsProvider(options.pid, options.fields,
- options.debugfs_include_past))
- if options.tracepoints or not providers:
- providers.append(TracepointProvider(options.pid, options.fields))
- return providers
- def _update_provider_filters(self):
- """Propagates fields filters to providers."""
- # As we reset the counters when updating the fields we can
- # also clear the cache of old values.
- self.values = {}
- for provider in self.providers:
- provider.update_fields(self._fields_filter)
- def reset(self):
- self.values = {}
- for provider in self.providers:
- provider.reset()
- @property
- def fields_filter(self):
- return self._fields_filter
- @fields_filter.setter
- def fields_filter(self, fields_filter):
- if fields_filter != self._fields_filter:
- self._fields_filter = fields_filter
- self._update_provider_filters()
- @property
- def pid_filter(self):
- return self._pid_filter
- @pid_filter.setter
- def pid_filter(self, pid):
- if pid != self._pid_filter:
- self._pid_filter = pid
- self.values = {}
- for provider in self.providers:
- provider.pid = self._pid_filter
- @property
- def child_events(self):
- return self._child_events
- @child_events.setter
- def child_events(self, val):
- self._child_events = val
- for provider in self.providers:
- provider.child_events = val
- def get(self, by_guest=0):
- """Returns a dict with field -> (value, delta to last value) of all
- provider data.
- Key formats:
- * plain: 'key' is event name
- * child-parent: 'key' is in format '<child> <parent>'
- * pid: 'key' is the pid of the guest, and the record contains the
- aggregated event data
- These formats are generated by the providers, and handled in class TUI.
- """
- for provider in self.providers:
- new = provider.read(by_guest=by_guest)
- for key in new:
- oldval = self.values.get(key, EventStat(0, 0)).value
- newval = new.get(key, 0)
- newdelta = newval - oldval
- self.values[key] = EventStat(newval, newdelta)
- return self.values
- def toggle_display_guests(self, to_pid):
- """Toggle between collection of stats by individual event and by
- guest pid
- Events reported by DebugfsProvider change when switching to/from
- reading by guest values. Hence we have to remove the excess event
- names from self.values.
- """
- if any(isinstance(ins, TracepointProvider) for ins in self.providers):
- return 1
- if to_pid:
- for provider in self.providers:
- if isinstance(provider, DebugfsProvider):
- for key in provider.fields:
- if key in self.values.keys():
- del self.values[key]
- else:
- oldvals = self.values.copy()
- for key in oldvals:
- if key.isdigit():
- del self.values[key]
- # Update oldval (see get())
- self.get(to_pid)
- return 0
- DELAY_DEFAULT = 3.0
- MAX_GUEST_NAME_LEN = 48
- MAX_REGEX_LEN = 44
- SORT_DEFAULT = 0
- MIN_DELAY = 0.1
- MAX_DELAY = 25.5
- class Tui(object):
- """Instruments curses to draw a nice text ui."""
- def __init__(self, stats, opts):
- self.stats = stats
- self.screen = None
- self._delay_initial = 0.25
- self._delay_regular = opts.set_delay
- self._sorting = SORT_DEFAULT
- self._display_guests = 0
- def __enter__(self):
- """Initialises curses for later use. Based on curses.wrapper
- implementation from the Python standard library."""
- self.screen = curses.initscr()
- curses.noecho()
- curses.cbreak()
- # The try/catch works around a minor bit of
- # over-conscientiousness in the curses module, the error
- # return from C start_color() is ignorable.
- try:
- curses.start_color()
- except curses.error:
- pass
- # Hide cursor in extra statement as some monochrome terminals
- # might support hiding but not colors.
- try:
- curses.curs_set(0)
- except curses.error:
- pass
- curses.use_default_colors()
- return self
- def __exit__(self, *exception):
- """Resets the terminal to its normal state. Based on curses.wrapper
- implementation from the Python standard library."""
- if self.screen:
- self.screen.keypad(0)
- curses.echo()
- curses.nocbreak()
- curses.endwin()
- @staticmethod
- def get_all_gnames():
- """Returns a list of (pid, gname) tuples of all running guests"""
- res = []
- try:
- child = subprocess.Popen(['ps', '-A', '--format', 'pid,args'],
- stdout=subprocess.PIPE)
- except:
- raise Exception
- for line in child.stdout:
- line = line.decode(ENCODING).lstrip().split(' ', 1)
- # perform a sanity check before calling the more expensive
- # function to possibly extract the guest name
- if ' -name ' in line[1]:
- res.append((line[0], Tui.get_gname_from_pid(line[0])))
- child.stdout.close()
- return res
- def _print_all_gnames(self, row):
- """Print a list of all running guests along with their pids."""
- self.screen.addstr(row, 2, '%8s %-60s' %
- ('Pid', 'Guest Name (fuzzy list, might be '
- 'inaccurate!)'),
- curses.A_UNDERLINE)
- row += 1
- try:
- for line in self.get_all_gnames():
- self.screen.addstr(row, 2, '%8s %-60s' % (line[0], line[1]))
- row += 1
- if row >= self.screen.getmaxyx()[0]:
- break
- except Exception:
- self.screen.addstr(row + 1, 2, 'Not available')
- @staticmethod
- def get_pid_from_gname(gname):
- """Fuzzy function to convert guest name to QEMU process pid.
- Returns a list of potential pids, can be empty if no match found.
- Throws an exception on processing errors.
- """
- pids = []
- for line in Tui.get_all_gnames():
- if gname == line[1]:
- pids.append(int(line[0]))
- return pids
- @staticmethod
- def get_gname_from_pid(pid):
- """Returns the guest name for a QEMU process pid.
- Extracts the guest name from the QEMU comma line by processing the
- '-name' option. Will also handle names specified out of sequence.
- """
- name = ''
- try:
- line = open('/proc/{}/cmdline'
- .format(pid), 'r').read().split('\0')
- parms = line[line.index('-name') + 1].split(',')
- while '' in parms:
- # commas are escaped (i.e. ',,'), hence e.g. 'foo,bar' results
- # in # ['foo', '', 'bar'], which we revert here
- idx = parms.index('')
- parms[idx - 1] += ',' + parms[idx + 1]
- del parms[idx:idx+2]
- # the '-name' switch allows for two ways to specify the guest name,
- # where the plain name overrides the name specified via 'guest='
- for arg in parms:
- if '=' not in arg:
- name = arg
- break
- if arg[:6] == 'guest=':
- name = arg[6:]
- except (ValueError, IOError, IndexError):
- pass
- return name
- def _update_pid(self, pid):
- """Propagates pid selection to stats object."""
- self.screen.addstr(4, 1, 'Updating pid filter...')
- self.screen.refresh()
- self.stats.pid_filter = pid
- def _refresh_header(self, pid=None):
- """Refreshes the header."""
- if pid is None:
- pid = self.stats.pid_filter
- self.screen.erase()
- gname = self.get_gname_from_pid(pid)
- self._gname = gname
- if gname:
- gname = ('({})'.format(gname[:MAX_GUEST_NAME_LEN] + '...'
- if len(gname) > MAX_GUEST_NAME_LEN
- else gname))
- if pid > 0:
- self._headline = 'kvm statistics - pid {0} {1}'.format(pid, gname)
- else:
- self._headline = 'kvm statistics - summary'
- self.screen.addstr(0, 0, self._headline, curses.A_BOLD)
- if self.stats.fields_filter:
- regex = self.stats.fields_filter
- if len(regex) > MAX_REGEX_LEN:
- regex = regex[:MAX_REGEX_LEN] + '...'
- self.screen.addstr(1, 17, 'regex filter: {0}'.format(regex))
- if self._display_guests:
- col_name = 'Guest Name'
- else:
- col_name = 'Event'
- self.screen.addstr(2, 1, '%-40s %10s%7s %8s' %
- (col_name, 'Total', '%Total', 'CurAvg/s'),
- curses.A_STANDOUT)
- self.screen.addstr(4, 1, 'Collecting data...')
- self.screen.refresh()
- def _refresh_body(self, sleeptime):
- def insert_child(sorted_items, child, values, parent):
- num = len(sorted_items)
- for i in range(0, num):
- # only add child if parent is present
- if parent.startswith(sorted_items[i][0]):
- sorted_items.insert(i + 1, (' ' + child, values))
- def get_sorted_events(self, stats):
- """ separate parent and child events """
- if self._sorting == SORT_DEFAULT:
- def sortkey(pair):
- # sort by (delta value, overall value)
- v = pair[1]
- return (v.delta, v.value)
- else:
- def sortkey(pair):
- # sort by overall value
- v = pair[1]
- return v.value
- childs = []
- sorted_items = []
- # we can't rule out child events to appear prior to parents even
- # when sorted - separate out all children first, and add in later
- for key, values in sorted(stats.items(), key=sortkey,
- reverse=True):
- if values == (0, 0):
- continue
- if key.find(' ') != -1:
- if not self.stats.child_events:
- continue
- childs.insert(0, (key, values))
- else:
- sorted_items.append((key, values))
- if self.stats.child_events:
- for key, values in childs:
- (child, parent) = key.split(' ')
- insert_child(sorted_items, child, values, parent)
- return sorted_items
- if not self._is_running_guest(self.stats.pid_filter):
- if self._gname:
- try: # ...to identify the guest by name in case it's back
- pids = self.get_pid_from_gname(self._gname)
- if len(pids) == 1:
- self._refresh_header(pids[0])
- self._update_pid(pids[0])
- return
- except:
- pass
- self._display_guest_dead()
- # leave final data on screen
- return
- row = 3
- self.screen.move(row, 0)
- self.screen.clrtobot()
- stats = self.stats.get(self._display_guests)
- total = 0.
- ctotal = 0.
- for key, values in stats.items():
- if self._display_guests:
- if self.get_gname_from_pid(key):
- total += values.value
- continue
- if not key.find(' ') != -1:
- total += values.value
- else:
- ctotal += values.value
- if total == 0.:
- # we don't have any fields, or all non-child events are filtered
- total = ctotal
- # print events
- tavg = 0
- tcur = 0
- guest_removed = False
- for key, values in get_sorted_events(self, stats):
- if row >= self.screen.getmaxyx()[0] - 1 or values == (0, 0):
- break
- if self._display_guests:
- key = self.get_gname_from_pid(key)
- if not key:
- continue
- cur = int(round(values.delta / sleeptime)) if values.delta else 0
- if cur < 0:
- guest_removed = True
- continue
- if key[0] != ' ':
- if values.delta:
- tcur += values.delta
- ptotal = values.value
- ltotal = total
- else:
- ltotal = ptotal
- self.screen.addstr(row, 1, '%-40s %10d%7.1f %8s' % (key,
- values.value,
- values.value * 100 / float(ltotal), cur))
- row += 1
- if row == 3:
- if guest_removed:
- self.screen.addstr(4, 1, 'Guest removed, updating...')
- else:
- self.screen.addstr(4, 1, 'No matching events reported yet')
- if row > 4:
- tavg = int(round(tcur / sleeptime)) if tcur > 0 else ''
- self.screen.addstr(row, 1, '%-40s %10d %8s' %
- ('Total', total, tavg), curses.A_BOLD)
- self.screen.refresh()
- def _display_guest_dead(self):
- marker = ' Guest is DEAD '
- y = min(len(self._headline), 80 - len(marker))
- self.screen.addstr(0, y, marker, curses.A_BLINK | curses.A_STANDOUT)
- def _show_msg(self, text):
- """Display message centered text and exit on key press"""
- hint = 'Press any key to continue'
- curses.cbreak()
- self.screen.erase()
- (x, term_width) = self.screen.getmaxyx()
- row = 2
- for line in text:
- start = (term_width - len(line)) // 2
- self.screen.addstr(row, start, line)
- row += 1
- self.screen.addstr(row + 1, (term_width - len(hint)) // 2, hint,
- curses.A_STANDOUT)
- self.screen.getkey()
- def _show_help_interactive(self):
- """Display help with list of interactive commands"""
- msg = (' b toggle events by guests (debugfs only, honors'
- ' filters)',
- ' c clear filter',
- ' f filter by regular expression',
- ' g filter by guest name/PID',
- ' h display interactive commands reference',
- ' o toggle sorting order (Total vs CurAvg/s)',
- ' p filter by guest name/PID',
- ' q quit',
- ' r reset stats',
- ' s set delay between refreshs (value range: '
- '%s-%s secs)' % (MIN_DELAY, MAX_DELAY),
- ' x toggle reporting of stats for individual child trace'
- ' events',
- 'Any other key refreshes statistics immediately')
- curses.cbreak()
- self.screen.erase()
- self.screen.addstr(0, 0, "Interactive commands reference",
- curses.A_BOLD)
- self.screen.addstr(2, 0, "Press any key to exit", curses.A_STANDOUT)
- row = 4
- for line in msg:
- self.screen.addstr(row, 0, line)
- row += 1
- self.screen.getkey()
- self._refresh_header()
- def _show_filter_selection(self):
- """Draws filter selection mask.
- Asks for a valid regex and sets the fields filter accordingly.
- """
- msg = ''
- while True:
- self.screen.erase()
- self.screen.addstr(0, 0,
- "Show statistics for events matching a regex.",
- curses.A_BOLD)
- self.screen.addstr(2, 0,
- "Current regex: {0}"
- .format(self.stats.fields_filter))
- self.screen.addstr(5, 0, msg)
- self.screen.addstr(3, 0, "New regex: ")
- curses.echo()
- regex = self.screen.getstr().decode(ENCODING)
- curses.noecho()
- if len(regex) == 0:
- self.stats.fields_filter = ''
- self._refresh_header()
- return
- try:
- re.compile(regex)
- self.stats.fields_filter = regex
- self._refresh_header()
- return
- except re.error:
- msg = '"' + regex + '": Not a valid regular expression'
- continue
- def _show_set_update_interval(self):
- """Draws update interval selection mask."""
- msg = ''
- while True:
- self.screen.erase()
- self.screen.addstr(0, 0, 'Set update interval (defaults to %.1fs).'
- % DELAY_DEFAULT, curses.A_BOLD)
- self.screen.addstr(4, 0, msg)
- self.screen.addstr(2, 0, 'Change delay from %.1fs to ' %
- self._delay_regular)
- curses.echo()
- val = self.screen.getstr().decode(ENCODING)
- curses.noecho()
- try:
- if len(val) > 0:
- delay = float(val)
- err = is_delay_valid(delay)
- if err is not None:
- msg = err
- continue
- else:
- delay = DELAY_DEFAULT
- self._delay_regular = delay
- break
- except ValueError:
- msg = '"' + str(val) + '": Invalid value'
- self._refresh_header()
- def _is_running_guest(self, pid):
- """Check if pid is still a running process."""
- if not pid:
- return True
- return os.path.isdir(os.path.join('/proc/', str(pid)))
- def _show_vm_selection_by_guest(self):
- """Draws guest selection mask.
- Asks for a guest name or pid until a valid guest name or '' is entered.
- """
- msg = ''
- while True:
- self.screen.erase()
- self.screen.addstr(0, 0,
- 'Show statistics for specific guest or pid.',
- curses.A_BOLD)
- self.screen.addstr(1, 0,
- 'This might limit the shown data to the trace '
- 'statistics.')
- self.screen.addstr(5, 0, msg)
- self._print_all_gnames(7)
- curses.echo()
- curses.curs_set(1)
- self.screen.addstr(3, 0, "Guest or pid [ENTER exits]: ")
- guest = self.screen.getstr().decode(ENCODING)
- curses.noecho()
- pid = 0
- if not guest or guest == '0':
- break
- if guest.isdigit():
- if not self._is_running_guest(guest):
- msg = '"' + guest + '": Not a running process'
- continue
- pid = int(guest)
- break
- pids = []
- try:
- pids = self.get_pid_from_gname(guest)
- except:
- msg = '"' + guest + '": Internal error while searching, ' \
- 'use pid filter instead'
- continue
- if len(pids) == 0:
- msg = '"' + guest + '": Not an active guest'
- continue
- if len(pids) > 1:
- msg = '"' + guest + '": Multiple matches found, use pid ' \
- 'filter instead'
- continue
- pid = pids[0]
- break
- curses.curs_set(0)
- self._refresh_header(pid)
- self._update_pid(pid)
- def show_stats(self):
- """Refreshes the screen and processes user input."""
- sleeptime = self._delay_initial
- self._refresh_header()
- start = 0.0 # result based on init value never appears on screen
- while True:
- self._refresh_body(time.time() - start)
- curses.halfdelay(int(sleeptime * 10))
- start = time.time()
- sleeptime = self._delay_regular
- try:
- char = self.screen.getkey()
- if char == 'b':
- self._display_guests = not self._display_guests
- if self.stats.toggle_display_guests(self._display_guests):
- self._show_msg(['Command not available with '
- 'tracepoints enabled', 'Restart with '
- 'debugfs only (see option \'-d\') and '
- 'try again!'])
- self._display_guests = not self._display_guests
- self._refresh_header()
- if char == 'c':
- self.stats.fields_filter = ''
- self._refresh_header(0)
- self._update_pid(0)
- if char == 'f':
- curses.curs_set(1)
- self._show_filter_selection()
- curses.curs_set(0)
- sleeptime = self._delay_initial
- if char == 'g' or char == 'p':
- self._show_vm_selection_by_guest()
- sleeptime = self._delay_initial
- if char == 'h':
- self._show_help_interactive()
- if char == 'o':
- self._sorting = not self._sorting
- if char == 'q':
- break
- if char == 'r':
- self.stats.reset()
- if char == 's':
- curses.curs_set(1)
- self._show_set_update_interval()
- curses.curs_set(0)
- sleeptime = self._delay_initial
- if char == 'x':
- self.stats.child_events = not self.stats.child_events
- except KeyboardInterrupt:
- break
- except curses.error:
- continue
- def batch(stats):
- """Prints statistics in a key, value format."""
- try:
- s = stats.get()
- time.sleep(1)
- s = stats.get()
- for key, values in sorted(s.items()):
- print('%-42s%10d%10d' % (key.split(' ')[0], values.value,
- values.delta))
- except KeyboardInterrupt:
- pass
- class StdFormat(object):
- def __init__(self, keys):
- self._banner = ''
- for key in keys:
- self._banner += key.split(' ')[0] + ' '
- def get_banner(self):
- return self._banner
- def get_statline(self, keys, s):
- res = ''
- for key in keys:
- res += ' %9d' % s[key].delta
- return res
- class CSVFormat(object):
- def __init__(self, keys):
- self._banner = 'timestamp'
- self._banner += reduce(lambda res, key: "{},{!s}".format(res,
- key.split(' ')[0]), keys, '')
- def get_banner(self):
- return self._banner
- def get_statline(self, keys, s):
- return reduce(lambda res, key: "{},{!s}".format(res, s[key].delta),
- keys, '')
- def log(stats, opts, frmt, keys):
- """Prints statistics as reiterating key block, multiple value blocks."""
- global signal_received
- line = 0
- banner_repeat = 20
- f = None
- def do_banner(opts):
- nonlocal f
- if opts.log_to_file:
- if not f:
- try:
- f = open(opts.log_to_file, 'a')
- except (IOError, OSError):
- sys.exit("Error: Could not open file: %s" %
- opts.log_to_file)
- if isinstance(frmt, CSVFormat) and f.tell() != 0:
- return
- print(frmt.get_banner(), file=f or sys.stdout)
- def do_statline(opts, values):
- statline = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + \
- frmt.get_statline(keys, values)
- print(statline, file=f or sys.stdout)
- do_banner(opts)
- banner_printed = True
- while True:
- try:
- time.sleep(opts.set_delay)
- if signal_received:
- banner_printed = True
- line = 0
- f.close()
- do_banner(opts)
- signal_received = False
- if (line % banner_repeat == 0 and not banner_printed and
- not (opts.log_to_file and isinstance(frmt, CSVFormat))):
- do_banner(opts)
- banner_printed = True
- values = stats.get()
- if (not opts.skip_zero_records or
- any(values[k].delta != 0 for k in keys)):
- do_statline(opts, values)
- line += 1
- banner_printed = False
- except KeyboardInterrupt:
- break
- if opts.log_to_file:
- f.close()
- def handle_signal(sig, frame):
- global signal_received
- signal_received = True
- return
- def is_delay_valid(delay):
- """Verify delay is in valid value range."""
- msg = None
- if delay < MIN_DELAY:
- msg = '"' + str(delay) + '": Delay must be >=%s' % MIN_DELAY
- if delay > MAX_DELAY:
- msg = '"' + str(delay) + '": Delay must be <=%s' % MAX_DELAY
- return msg
- def get_options():
- """Returns processed program arguments."""
- description_text = """
- This script displays various statistics about VMs running under KVM.
- The statistics are gathered from the KVM debugfs entries and / or the
- currently available perf traces.
- The monitoring takes additional cpu cycles and might affect the VM's
- performance.
- Requirements:
- - Access to:
- %s
- %s/events/*
- /proc/pid/task
- - /proc/sys/kernel/perf_event_paranoid < 1 if user has no
- CAP_SYS_ADMIN and perf events are used.
- - CAP_SYS_RESOURCE if the hard limit is not high enough to allow
- the large number of files that are possibly opened.
- Interactive Commands:
- b toggle events by guests (debugfs only, honors filters)
- c clear filter
- f filter by regular expression
- g filter by guest name
- h display interactive commands reference
- o toggle sorting order (Total vs CurAvg/s)
- p filter by PID
- q quit
- r reset stats
- s set update interval (value range: 0.1-25.5 secs)
- x toggle reporting of stats for individual child trace events
- Press any other key to refresh statistics immediately.
- """ % (PATH_DEBUGFS_KVM, PATH_DEBUGFS_TRACING)
- class Guest_to_pid(argparse.Action):
- def __call__(self, parser, namespace, values, option_string=None):
- try:
- pids = Tui.get_pid_from_gname(values)
- except:
- sys.exit('Error while searching for guest "{}". Use "-p" to '
- 'specify a pid instead?'.format(values))
- if len(pids) == 0:
- sys.exit('Error: No guest by the name "{}" found'
- .format(values))
- if len(pids) > 1:
- sys.exit('Error: Multiple processes found (pids: {}). Use "-p"'
- ' to specify the desired pid'
- .format(" ".join(map(str, pids))))
- namespace.pid = pids[0]
- argparser = argparse.ArgumentParser(description=description_text,
- formatter_class=argparse
- .RawTextHelpFormatter)
- argparser.add_argument('-1', '--once', '--batch',
- action='store_true',
- default=False,
- help='run in batch mode for one second',
- )
- argparser.add_argument('-c', '--csv',
- action='store_true',
- default=False,
- help='log in csv format - requires option -l/-L',
- )
- argparser.add_argument('-d', '--debugfs',
- action='store_true',
- default=False,
- help='retrieve statistics from debugfs',
- )
- argparser.add_argument('-f', '--fields',
- default='',
- help='''fields to display (regex)
- "-f help" for a list of available events''',
- )
- argparser.add_argument('-g', '--guest',
- type=str,
- help='restrict statistics to guest by name',
- action=Guest_to_pid,
- )
- argparser.add_argument('-i', '--debugfs-include-past',
- action='store_true',
- default=False,
- help='include all available data on past events for'
- ' debugfs',
- )
- argparser.add_argument('-l', '--log',
- action='store_true',
- default=False,
- help='run in logging mode (like vmstat)',
- )
- argparser.add_argument('-L', '--log-to-file',
- type=str,
- metavar='FILE',
- help="like '--log', but logging to a file"
- )
- argparser.add_argument('-p', '--pid',
- type=int,
- default=0,
- help='restrict statistics to pid',
- )
- argparser.add_argument('-s', '--set-delay',
- type=float,
- default=DELAY_DEFAULT,
- metavar='DELAY',
- help='set delay between refreshs (value range: '
- '%s-%s secs)' % (MIN_DELAY, MAX_DELAY),
- )
- argparser.add_argument('-t', '--tracepoints',
- action='store_true',
- default=False,
- help='retrieve statistics from tracepoints',
- )
- argparser.add_argument('-z', '--skip-zero-records',
- action='store_true',
- default=False,
- help='omit records with all zeros in logging mode',
- )
- options = argparser.parse_args()
- if options.csv and not (options.log or options.log_to_file):
- sys.exit('Error: Option -c/--csv requires -l/--log')
- if options.skip_zero_records and not (options.log or options.log_to_file):
- sys.exit('Error: Option -z/--skip-zero-records requires -l/-L')
- try:
- # verify that we were passed a valid regex up front
- re.compile(options.fields)
- except re.error:
- sys.exit('Error: "' + options.fields + '" is not a valid regular '
- 'expression')
- return options
- def check_access(options):
- """Exits if the current user can't access all needed directories."""
- if not os.path.exists(PATH_DEBUGFS_TRACING) and (options.tracepoints or
- not options.debugfs):
- sys.stderr.write("Please enable CONFIG_TRACING in your kernel "
- "when using the option -t (default).\n"
- "If it is enabled, make {0} readable by the "
- "current user.\n"
- .format(PATH_DEBUGFS_TRACING))
- if options.tracepoints:
- sys.exit(1)
- sys.stderr.write("Falling back to debugfs statistics!\n")
- options.debugfs = True
- time.sleep(5)
- return options
- def assign_globals():
- global PATH_DEBUGFS_KVM
- global PATH_DEBUGFS_TRACING
- debugfs = ''
- for line in open('/proc/mounts'):
- if line.split(' ')[2] == 'debugfs':
- debugfs = line.split(' ')[1]
- break
- if debugfs == '':
- sys.stderr.write("Please make sure that CONFIG_DEBUG_FS is enabled in "
- "your kernel, mounted and\nreadable by the current "
- "user:\n"
- "('mount -t debugfs debugfs /sys/kernel/debug')\n")
- sys.exit(1)
- PATH_DEBUGFS_KVM = os.path.join(debugfs, 'kvm')
- PATH_DEBUGFS_TRACING = os.path.join(debugfs, 'tracing')
- if not os.path.exists(PATH_DEBUGFS_KVM):
- sys.stderr.write("Please make sure that CONFIG_KVM is enabled in "
- "your kernel and that the modules are loaded.\n")
- sys.exit(1)
- def main():
- assign_globals()
- options = get_options()
- options = check_access(options)
- if (options.pid > 0 and
- not os.path.isdir(os.path.join('/proc/',
- str(options.pid)))):
- sys.stderr.write('Did you use a (unsupported) tid instead of a pid?\n')
- sys.exit('Specified pid does not exist.')
- err = is_delay_valid(options.set_delay)
- if err is not None:
- sys.exit('Error: ' + err)
- stats = Stats(options)
- if options.fields == 'help':
- stats.fields_filter = None
- event_list = []
- for key in stats.get().keys():
- event_list.append(key.split('(', 1)[0])
- sys.stdout.write(' ' + '\n '.join(sorted(set(event_list))) + '\n')
- sys.exit(0)
- if options.log or options.log_to_file:
- if options.log_to_file:
- signal.signal(signal.SIGHUP, handle_signal)
- keys = sorted(stats.get().keys())
- if options.csv:
- frmt = CSVFormat(keys)
- else:
- frmt = StdFormat(keys)
- log(stats, options, frmt, keys)
- elif not options.once:
- with Tui(stats, options) as tui:
- tui.show_stats()
- else:
- batch(stats)
- if __name__ == "__main__":
- main()
|