1820 lines
61 KiB
Python
Executable File
1820 lines
61 KiB
Python
Executable File
#!/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
|
|
|
|
"""
|
|
exempt_list = ['halt_poll_fail_ns', 'halt_poll_success_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(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()
|