| #!/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 <avi@redhat.com> |
| # |
| """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, |
| 'PENDING_INTERRUPT': 7, |
| 'NMI_WINDOW': 8, |
| 'TASK_SWITCH': 9, |
| 'CPUID': 10, |
| 'HLT': 12, |
| '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, |
| 'MWAIT_INSTRUCTION': 36, |
| 'MONITOR_INSTRUCTION': 39, |
| 'PAUSE_INSTRUCTION': 40, |
| 'MCE_DURING_VMENTRY': 41, |
| 'TPR_BELOW_THRESHOLD': 43, |
| 'APIC_ACCESS': 44, |
| 'EPT_VIOLATION': 48, |
| 'EPT_MISCONFIG': 49, |
| 'WBINVD': 54, |
| 'XSETBV': 55, |
| 'APIC_WRITE': 56, |
| 'INVPCID': 58, |
| } |
| |
| SVM_EXIT_REASONS = { |
| 'READ_CR0': 0x000, |
| 'READ_CR3': 0x003, |
| 'READ_CR4': 0x004, |
| 'READ_CR8': 0x008, |
| 'WRITE_CR0': 0x010, |
| '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, |
| '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, |
| 'NPF': 0x400, |
| } |
| |
| # EC definition of HSR (from arch/arm64/include/asm/kvm_arm.h) |
| AARCH64_EXIT_REASONS = { |
| 'UNKNOWN': 0x00, |
| 'WFI': 0x01, |
| 'CP15_32': 0x03, |
| 'CP15_64': 0x04, |
| 'CP14_MR': 0x05, |
| 'CP14_LS': 0x06, |
| 'FP_ASIMD': 0x07, |
| 'CP10_ID': 0x08, |
| 'CP14_64': 0x0C, |
| 'ILL_ISS': 0x0E, |
| 'SVC32': 0x11, |
| 'HVC32': 0x12, |
| 'SMC32': 0x13, |
| 'SVC64': 0x15, |
| 'HVC64': 0x16, |
| 'SMC64': 0x17, |
| 'SYS64': 0x18, |
| 'IABT': 0x20, |
| 'IABT_HYP': 0x21, |
| 'PC_ALIGN': 0x22, |
| 'DABT': 0x24, |
| 'DABT_HYP': 0x25, |
| 'SP_ALIGN': 0x26, |
| 'FP_EXC32': 0x28, |
| 'FP_EXC64': 0x2C, |
| 'SERROR': 0x2F, |
| 'BREAKPT': 0x30, |
| 'BREAKPT_HYP': 0x31, |
| 'SOFTSTP': 0x32, |
| 'SOFTSTP_HYP': 0x33, |
| 'WATCHPT': 0x34, |
| 'WATCHPT_HYP': 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, |
| } |
| |
| 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 |
| |
| """ |
| return self.walkdir(PATH_DEBUGFS_KVM)[2] |
| |
| 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(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(' ')[0] == '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() |