#
# This work is licensed under the terms of the GNU GPL, version 2. See
# the COPYING file in the top-level directory.
+"""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.
+"""
import curses
import sys
}
class Arch(object):
- """Class that encapsulates global architecture specific data like
- syscall and ioctl numbers.
+ """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_online_cpus():
+ """Returns a list of cpu id integers."""
with open('/sys/devices/system/cpu/online') as cpu_list:
cpu_string = cpu_list.readline()
return parse_int_list(cpu_string)
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_reasons:
syscall = libc.syscall
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),
self.read_format = PERF_FORMAT_GROUP
def perf_event_open(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 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))
PATH_DEBUGFS_KVM = '/sys/kernel/debug/kvm'
class Group(object):
+ """Represents a perf event group."""
+
def __init__(self):
self.events = []
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],
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.name = name
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 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')
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
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 TracepointProvider(object):
+ """Data provider for the stats class.
+
+ Manages the events/groups from which it acquires its data.
+
+ """
def __init__(self):
self.group_leaders = []
self.filters = get_filters()
self._pid = 0
def get_available_fields(self):
+ """Returns a list of available event's 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 = walkdir(path)[1]
extra = []
return fields
def setup_traces(self):
+ """Creates all event and group objects needed to be able to retrieve
+ data."""
if self._pid > 0:
# Fetch list of all threads of the monitored pid, as qemu
# starts a thread for each vcpu.
@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):
@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):
+ """Returns 'event name: current value' for all enabled events."""
ret = defaultdict(int)
for group in self.group_leaders:
for name, val in group.read().iteritems():
return ret
class DebugfsProvider(object):
+ """Provides data from the files that KVM creates in the kvm debugfs
+ folder."""
def __init__(self):
self._fields = self.get_available_fields()
self._pid = 0
self.do_read = True
def get_available_fields(self):
+ """"Returns a list of available fields.
+
+ The fields are all available KVM debugfs files
+
+ """
return walkdir(PATH_DEBUGFS_KVM)[2]
@property
return 0
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, providers, pid, fields=None):
self.providers = providers
self._pid_filter = pid
self.update_provider_filters()
def update_provider_filters(self):
+ """Propagates fields filters to providers."""
def wanted(key):
if not self._fields_filter:
return True
provider.fields = provider_fields
def update_provider_pid(self):
+ """Propagates pid filters to providers."""
for provider in self.providers:
provider.pid = self._pid_filter
self.update_provider_pid()
def get(self):
+ """Returns a dict with field -> (value, delta to last value) of all
+ provider data."""
for provider in self.providers:
new = provider.read()
for key in provider.fields:
NUMBER_WIDTH = 10
class Tui(object):
+ """Instruments curses to draw a nice text ui."""
def __init__(self, stats):
self.stats = stats
self.screen = None
curses.endwin()
def update_drilldown(self):
+ """Sets or removes a filter that only allows fields without braces."""
if not self.stats.fields_filter:
self.stats.fields_filter = r'^[^\(]*$'
self.stats.fields_filter = None
def update_pid(self, pid):
+ """Propagates pid selection to stats object."""
self.stats.pid_filter = pid
def refresh(self, sleeptime):
+ """Refreshes on-screen data."""
self.screen.erase()
if self.stats.pid_filter > 0:
self.screen.addstr(0, 0, 'kvm statistics - pid {0}'
self.screen.refresh()
def show_filter_selection(self):
+ """Draws filter selection mask.
+
+ Asks for a valid regex and sets the fields filter accordingly.
+
+ """
while True:
self.screen.erase()
self.screen.addstr(0, 0,
continue
def show_vm_selection(self):
+ """Draws PID selection mask.
+
+ Asks for a pid until a valid pid or 0 has been entered.
+
+ """
while True:
self.screen.erase()
self.screen.addstr(0, 0,
continue
def show_stats(self):
+ """Refreshes the screen and processes user input."""
sleeptime = 0.25
while True:
self.refresh(sleeptime)
continue
def batch(stats):
+ """Prints statistics in a key, value format."""
s = stats.get()
time.sleep(1)
s = stats.get()
print '%-42s%10d%10d' % (key, values[0], values[1])
def log(stats):
+ """Prints statistics as reiterating key block, multiple value blocks."""
keys = sorted(stats.get().iterkeys())
def banner():
for k in keys:
line += 1
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
return options
def get_providers(options):
+ """Returns a list of data providers depending on the passed options."""
providers = []
if options.tracepoints:
return providers
def check_access(options):
+ """Exits if the current user can't access all needed directories."""
if not os.path.exists('/sys/kernel/debug'):
sys.stderr.write('Please enable CONFIG_DEBUG_FS in your kernel.')
sys.exit(1)