|
@@ -295,114 +295,6 @@ class ArchS390(Arch):
|
|
|
ARCH = Arch.get_arch()
|
|
|
|
|
|
|
|
|
-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))
|
|
|
-
|
|
|
-
|
|
|
-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
|
|
|
-
|
|
|
-
|
|
|
-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 = []
|
|
|
- try:
|
|
|
- child = subprocess.Popen(['ps', '-A', '--format', 'pid,args'],
|
|
|
- stdout=subprocess.PIPE)
|
|
|
- except:
|
|
|
- raise Exception
|
|
|
- for line in child.stdout:
|
|
|
- line = line.lstrip().split(' ', 1)
|
|
|
- # perform a sanity check before calling the more expensive
|
|
|
- # function to possibly extract the guest name
|
|
|
- if ' -name ' in line[1] and gname == get_gname_from_pid(line[0]):
|
|
|
- pids.append(int(line[0]))
|
|
|
- child.stdout.close()
|
|
|
-
|
|
|
- return pids
|
|
|
-
|
|
|
-
|
|
|
-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), 'rb').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 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:
|
|
|
- filters['kvm_exit'] = ('exit_reason', ARCH.exit_reasons)
|
|
|
- return filters
|
|
|
-
|
|
|
-libc = ctypes.CDLL('libc.so.6', use_errno=True)
|
|
|
-syscall = libc.syscall
|
|
|
-
|
|
|
-
|
|
|
class perf_event_attr(ctypes.Structure):
|
|
|
"""Struct that holds the necessary data to set up a trace event.
|
|
|
|
|
@@ -432,25 +324,6 @@ class perf_event_attr(ctypes.Structure):
|
|
|
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))
|
|
|
-
|
|
|
PERF_TYPE_TRACEPOINT = 2
|
|
|
PERF_FORMAT_GROUP = 1 << 3
|
|
|
|
|
@@ -495,6 +368,8 @@ 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,
|
|
@@ -511,6 +386,25 @@ class Event(object):
|
|
|
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."""
|
|
|
|
|
@@ -539,8 +433,8 @@ class Event(object):
|
|
|
if group.events:
|
|
|
group_leader = group.events[0].fd
|
|
|
|
|
|
- fd = perf_event_open(event_attr, trace_pid,
|
|
|
- trace_cpu, group_leader, 0)
|
|
|
+ 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),
|
|
@@ -575,17 +469,53 @@ class Event(object):
|
|
|
fcntl.ioctl(self.fd, ARCH.ioctl_numbers['RESET'], 0)
|
|
|
|
|
|
|
|
|
-class TracepointProvider(object):
|
|
|
+class Provider(object):
|
|
|
+ """Encapsulates functionalities used by all providers."""
|
|
|
+ @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):
|
|
|
+ def __init__(self, pid, fields_filter):
|
|
|
self.group_leaders = []
|
|
|
- self.filters = get_filters()
|
|
|
- self._fields = self.get_available_fields()
|
|
|
- self._pid = 0
|
|
|
+ self.filters = self.get_filters()
|
|
|
+ self.update_fields(fields_filter)
|
|
|
+ self.pid = 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_reasons:
|
|
|
+ filters['kvm_exit'] = ('exit_reason', ARCH.exit_reasons)
|
|
|
+ return filters
|
|
|
|
|
|
def get_available_fields(self):
|
|
|
"""Returns a list of available event's of format 'event name(filter
|
|
@@ -603,7 +533,7 @@ class TracepointProvider(object):
|
|
|
|
|
|
"""
|
|
|
path = os.path.join(PATH_DEBUGFS_TRACING, 'events', 'kvm')
|
|
|
- fields = walkdir(path)[1]
|
|
|
+ fields = self.walkdir(path)[1]
|
|
|
extra = []
|
|
|
for field in fields:
|
|
|
if field in self.filters:
|
|
@@ -613,6 +543,34 @@ class TracepointProvider(object):
|
|
|
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)]
|
|
|
+
|
|
|
+ @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."""
|
|
@@ -621,9 +579,9 @@ class TracepointProvider(object):
|
|
|
# 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 = walkdir(path)[1]
|
|
|
+ groupids = self.walkdir(path)[1]
|
|
|
else:
|
|
|
- groupids = get_online_cpus()
|
|
|
+ groupids = self.get_online_cpus()
|
|
|
|
|
|
# The constant is needed as a buffer for python libs, std
|
|
|
# streams and other files that the script opens.
|
|
@@ -671,9 +629,6 @@ class TracepointProvider(object):
|
|
|
|
|
|
self.group_leaders.append(group)
|
|
|
|
|
|
- def available_fields(self):
|
|
|
- return self.get_available_fields()
|
|
|
-
|
|
|
@property
|
|
|
def fields(self):
|
|
|
return self._fields
|
|
@@ -707,7 +662,7 @@ class TracepointProvider(object):
|
|
|
self.setup_traces()
|
|
|
self.fields = self._fields
|
|
|
|
|
|
- def read(self):
|
|
|
+ def read(self, by_guest=0):
|
|
|
"""Returns 'event name: current value' for all enabled events."""
|
|
|
ret = defaultdict(int)
|
|
|
for group in self.group_leaders:
|
|
@@ -723,16 +678,17 @@ class TracepointProvider(object):
|
|
|
event.reset()
|
|
|
|
|
|
|
|
|
-class DebugfsProvider(object):
|
|
|
+class DebugfsProvider(Provider):
|
|
|
"""Provides data from the files that KVM creates in the kvm debugfs
|
|
|
folder."""
|
|
|
- def __init__(self):
|
|
|
- self._fields = self.get_available_fields()
|
|
|
+ def __init__(self, pid, fields_filter, include_past):
|
|
|
+ self.update_fields(fields_filter)
|
|
|
self._baseline = {}
|
|
|
- self._pid = 0
|
|
|
self.do_read = True
|
|
|
self.paths = []
|
|
|
- self.reset()
|
|
|
+ self.pid = pid
|
|
|
+ if include_past:
|
|
|
+ self.restore()
|
|
|
|
|
|
def get_available_fields(self):
|
|
|
""""Returns a list of available fields.
|
|
@@ -740,7 +696,12 @@ class DebugfsProvider(object):
|
|
|
The fields are all available KVM debugfs files
|
|
|
|
|
|
"""
|
|
|
- return walkdir(PATH_DEBUGFS_KVM)[2]
|
|
|
+ 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)]
|
|
|
|
|
|
@property
|
|
|
def fields(self):
|
|
@@ -757,10 +718,9 @@ class DebugfsProvider(object):
|
|
|
|
|
|
@pid.setter
|
|
|
def pid(self, pid):
|
|
|
+ self._pid = pid
|
|
|
if pid != 0:
|
|
|
- self._pid = pid
|
|
|
-
|
|
|
- vms = walkdir(PATH_DEBUGFS_KVM)[1]
|
|
|
+ vms = self.walkdir(PATH_DEBUGFS_KVM)[1]
|
|
|
if len(vms) == 0:
|
|
|
self.do_read = False
|
|
|
|
|
@@ -771,8 +731,15 @@ class DebugfsProvider(object):
|
|
|
self.do_read = True
|
|
|
self.reset()
|
|
|
|
|
|
- def read(self, reset=0):
|
|
|
- """Returns a dict with format:'file name / field -> current value'."""
|
|
|
+ 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.
|
|
@@ -789,12 +756,22 @@ class DebugfsProvider(object):
|
|
|
for field in self._fields:
|
|
|
value = self.read_field(field, path)
|
|
|
key = path + field
|
|
|
- if reset:
|
|
|
+ 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
|
|
|
- results[field] = (results.get(field, 0) + value -
|
|
|
- self._baseline.get(key, 0))
|
|
|
+ increment = (results.get(field, 0) + value -
|
|
|
+ self._baseline.get(key, 0))
|
|
|
+ if by_guest:
|
|
|
+ pid = key.split('-')[0]
|
|
|
+ if pid in results:
|
|
|
+ results[pid] += increment
|
|
|
+ else:
|
|
|
+ results[pid] = increment
|
|
|
+ else:
|
|
|
+ results[field] = increment
|
|
|
|
|
|
return results
|
|
|
|
|
@@ -813,6 +790,11 @@ class DebugfsProvider(object):
|
|
|
self._baseline = {}
|
|
|
self.read(1)
|
|
|
|
|
|
+ def restore(self):
|
|
|
+ """Reset field counters"""
|
|
|
+ self._baseline = {}
|
|
|
+ self.read(2)
|
|
|
+
|
|
|
|
|
|
class Stats(object):
|
|
|
"""Manages the data providers and the data they provide.
|
|
@@ -821,33 +803,32 @@ class Stats(object):
|
|
|
provider data.
|
|
|
|
|
|
"""
|
|
|
- def __init__(self, providers, pid, fields=None):
|
|
|
- self.providers = providers
|
|
|
- self._pid_filter = pid
|
|
|
- self._fields_filter = fields
|
|
|
+ def __init__(self, options):
|
|
|
+ self.providers = self.get_providers(options)
|
|
|
+ self._pid_filter = options.pid
|
|
|
+ self._fields_filter = options.fields
|
|
|
self.values = {}
|
|
|
- self.update_provider_pid()
|
|
|
- self.update_provider_filters()
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def get_providers(options):
|
|
|
+ """Returns a list of data providers depending on the passed options."""
|
|
|
+ providers = []
|
|
|
+
|
|
|
+ if options.debugfs:
|
|
|
+ providers.append(DebugfsProvider(options.pid, options.fields,
|
|
|
+ options.dbgfs_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."""
|
|
|
- def wanted(key):
|
|
|
- if not self._fields_filter:
|
|
|
- return True
|
|
|
- return re.match(self._fields_filter, key) is not None
|
|
|
-
|
|
|
# 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_fields = [key for key in provider.get_available_fields()
|
|
|
- if wanted(key)]
|
|
|
- provider.fields = provider_fields
|
|
|
-
|
|
|
- def update_provider_pid(self):
|
|
|
- """Propagates pid filters to providers."""
|
|
|
- for provider in self.providers:
|
|
|
- provider.pid = self._pid_filter
|
|
|
+ provider.update_fields(self._fields_filter)
|
|
|
|
|
|
def reset(self):
|
|
|
self.values = {}
|
|
@@ -873,27 +854,52 @@ class Stats(object):
|
|
|
if pid != self._pid_filter:
|
|
|
self._pid_filter = pid
|
|
|
self.values = {}
|
|
|
- self.update_provider_pid()
|
|
|
+ for provider in self.providers:
|
|
|
+ provider.pid = self._pid_filter
|
|
|
|
|
|
- def get(self):
|
|
|
+ def get(self, by_guest=0):
|
|
|
"""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:
|
|
|
+ new = provider.read(by_guest=by_guest)
|
|
|
+ for key in new if by_guest else provider.fields:
|
|
|
oldval = self.values.get(key, (0, 0))[0]
|
|
|
newval = new.get(key, 0)
|
|
|
newdelta = newval - oldval
|
|
|
self.values[key] = (newval, newdelta)
|
|
|
return self.values
|
|
|
|
|
|
-LABEL_WIDTH = 40
|
|
|
-NUMBER_WIDTH = 10
|
|
|
-DELAY_INITIAL = 0.25
|
|
|
-DELAY_REGULAR = 3.0
|
|
|
+ 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
|
|
|
DEFAULT_REGEX = r'^[^\(]*$'
|
|
|
+SORT_DEFAULT = 0
|
|
|
|
|
|
|
|
|
class Tui(object):
|
|
@@ -901,7 +907,10 @@ class Tui(object):
|
|
|
def __init__(self, stats):
|
|
|
self.stats = stats
|
|
|
self.screen = None
|
|
|
- self.update_drilldown()
|
|
|
+ self._delay_initial = 0.25
|
|
|
+ self._delay_regular = DELAY_DEFAULT
|
|
|
+ self._sorting = SORT_DEFAULT
|
|
|
+ self._display_guests = 0
|
|
|
|
|
|
def __enter__(self):
|
|
|
"""Initialises curses for later use. Based on curses.wrapper
|
|
@@ -929,7 +938,7 @@ class Tui(object):
|
|
|
return self
|
|
|
|
|
|
def __exit__(self, *exception):
|
|
|
- """Resets the terminal to its normal state. Based on curses.wrappre
|
|
|
+ """Resets the terminal to its normal state. Based on curses.wrapper
|
|
|
implementation from the Python standard library."""
|
|
|
if self.screen:
|
|
|
self.screen.keypad(0)
|
|
@@ -937,6 +946,86 @@ class Tui(object):
|
|
|
curses.nocbreak()
|
|
|
curses.endwin()
|
|
|
|
|
|
+ def get_all_gnames(self):
|
|
|
+ """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.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], self.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')
|
|
|
+
|
|
|
+ def get_pid_from_gname(self, 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 self.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), 'rb').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_drilldown(self):
|
|
|
"""Sets or removes a filter that only allows fields without braces."""
|
|
|
if not self.stats.fields_filter:
|
|
@@ -954,7 +1043,7 @@ class Tui(object):
|
|
|
if pid is None:
|
|
|
pid = self.stats.pid_filter
|
|
|
self.screen.erase()
|
|
|
- gname = get_gname_from_pid(pid)
|
|
|
+ gname = self.get_gname_from_pid(pid)
|
|
|
if gname:
|
|
|
gname = ('({})'.format(gname[:MAX_GUEST_NAME_LEN] + '...'
|
|
|
if len(gname) > MAX_GUEST_NAME_LEN
|
|
@@ -970,13 +1059,13 @@ class Tui(object):
|
|
|
if len(regex) > MAX_REGEX_LEN:
|
|
|
regex = regex[:MAX_REGEX_LEN] + '...'
|
|
|
self.screen.addstr(1, 17, 'regex filter: {0}'.format(regex))
|
|
|
- self.screen.addstr(2, 1, 'Event')
|
|
|
- self.screen.addstr(2, 1 + LABEL_WIDTH + NUMBER_WIDTH -
|
|
|
- len('Total'), 'Total')
|
|
|
- self.screen.addstr(2, 1 + LABEL_WIDTH + NUMBER_WIDTH + 7 -
|
|
|
- len('%Total'), '%Total')
|
|
|
- self.screen.addstr(2, 1 + LABEL_WIDTH + NUMBER_WIDTH + 7 + 8 -
|
|
|
- len('Current'), 'Current')
|
|
|
+ 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()
|
|
|
|
|
@@ -984,16 +1073,25 @@ class Tui(object):
|
|
|
row = 3
|
|
|
self.screen.move(row, 0)
|
|
|
self.screen.clrtobot()
|
|
|
- stats = self.stats.get()
|
|
|
+ stats = self.stats.get(self._display_guests)
|
|
|
|
|
|
- def sortkey(x):
|
|
|
+ def sortCurAvg(x):
|
|
|
+ # sort by current events if available
|
|
|
if stats[x][1]:
|
|
|
return (-stats[x][1], -stats[x][0])
|
|
|
else:
|
|
|
return (0, -stats[x][0])
|
|
|
+
|
|
|
+ def sortTotal(x):
|
|
|
+ # sort by totals
|
|
|
+ return (0, -stats[x][0])
|
|
|
total = 0.
|
|
|
for val in stats.values():
|
|
|
total += val[0]
|
|
|
+ if self._sorting == SORT_DEFAULT:
|
|
|
+ sortkey = sortCurAvg
|
|
|
+ else:
|
|
|
+ sortkey = sortTotal
|
|
|
for key in sorted(stats.keys(), key=sortkey):
|
|
|
|
|
|
if row >= self.screen.getmaxyx()[0]:
|
|
@@ -1001,18 +1099,61 @@ class Tui(object):
|
|
|
values = stats[key]
|
|
|
if not values[0] and not values[1]:
|
|
|
break
|
|
|
- col = 1
|
|
|
- self.screen.addstr(row, col, key)
|
|
|
- col += LABEL_WIDTH
|
|
|
- self.screen.addstr(row, col, '%10d' % (values[0],))
|
|
|
- col += NUMBER_WIDTH
|
|
|
- self.screen.addstr(row, col, '%7.1f' % (values[0] * 100 / total,))
|
|
|
- col += 7
|
|
|
- if values[1] is not None:
|
|
|
- self.screen.addstr(row, col, '%8d' % (values[1] / sleeptime,))
|
|
|
+ if values[0] is not None:
|
|
|
+ cur = int(round(values[1] / sleeptime)) if values[1] else ''
|
|
|
+ if self._display_guests:
|
|
|
+ key = self.get_gname_from_pid(key)
|
|
|
+ self.screen.addstr(row, 1, '%-40s %10d%7.1f %8s' %
|
|
|
+ (key, values[0], values[0] * 100 / total,
|
|
|
+ cur))
|
|
|
row += 1
|
|
|
+ if row == 3:
|
|
|
+ self.screen.addstr(4, 1, 'No matching events reported yet')
|
|
|
self.screen.refresh()
|
|
|
|
|
|
+ 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',
|
|
|
+ ' 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',
|
|
|
+ ' 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.
|
|
|
|
|
@@ -1059,6 +1200,7 @@ class Tui(object):
|
|
|
'This might limit the shown data to the trace '
|
|
|
'statistics.')
|
|
|
self.screen.addstr(5, 0, msg)
|
|
|
+ self.print_all_gnames(7)
|
|
|
|
|
|
curses.echo()
|
|
|
self.screen.addstr(3, 0, "Pid [0 or pid]: ")
|
|
@@ -1077,10 +1219,40 @@ class Tui(object):
|
|
|
self.refresh_header(pid)
|
|
|
self.update_pid(pid)
|
|
|
break
|
|
|
-
|
|
|
except ValueError:
|
|
|
msg = '"' + str(pid) + '": Not a valid pid'
|
|
|
- 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 %fs).' %
|
|
|
+ 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()
|
|
|
+ curses.noecho()
|
|
|
+
|
|
|
+ try:
|
|
|
+ if len(val) > 0:
|
|
|
+ delay = float(val)
|
|
|
+ if delay < 0.1:
|
|
|
+ msg = '"' + str(val) + '": Value must be >=0.1'
|
|
|
+ continue
|
|
|
+ if delay > 25.5:
|
|
|
+ msg = '"' + str(val) + '": Value must be <=25.5'
|
|
|
+ continue
|
|
|
+ else:
|
|
|
+ delay = DELAY_DEFAULT
|
|
|
+ self._delay_regular = delay
|
|
|
+ break
|
|
|
+
|
|
|
+ except ValueError:
|
|
|
+ msg = '"' + str(val) + '": Invalid value'
|
|
|
+ self.refresh_header()
|
|
|
|
|
|
def show_vm_selection_by_guest_name(self):
|
|
|
"""Draws guest selection mask.
|
|
@@ -1098,6 +1270,7 @@ class Tui(object):
|
|
|
'This might limit the shown data to the trace '
|
|
|
'statistics.')
|
|
|
self.screen.addstr(5, 0, msg)
|
|
|
+ self.print_all_gnames(7)
|
|
|
curses.echo()
|
|
|
self.screen.addstr(3, 0, "Guest [ENTER or guest]: ")
|
|
|
gname = self.screen.getstr()
|
|
@@ -1110,7 +1283,7 @@ class Tui(object):
|
|
|
else:
|
|
|
pids = []
|
|
|
try:
|
|
|
- pids = get_pid_from_gname(gname)
|
|
|
+ pids = self.get_pid_from_gname(gname)
|
|
|
except:
|
|
|
msg = '"' + gname + '": Internal error while searching, ' \
|
|
|
'use pid filter instead'
|
|
@@ -1128,38 +1301,60 @@ class Tui(object):
|
|
|
|
|
|
def show_stats(self):
|
|
|
"""Refreshes the screen and processes user input."""
|
|
|
- sleeptime = DELAY_INITIAL
|
|
|
+ sleeptime = self._delay_initial
|
|
|
self.refresh_header()
|
|
|
+ start = 0.0 # result based on init value never appears on screen
|
|
|
while True:
|
|
|
- self.refresh_body(sleeptime)
|
|
|
+ self.refresh_body(time.time() - start)
|
|
|
curses.halfdelay(int(sleeptime * 10))
|
|
|
- sleeptime = DELAY_REGULAR
|
|
|
+ start = time.time()
|
|
|
+ sleeptime = self._delay_regular
|
|
|
try:
|
|
|
char = self.screen.getkey()
|
|
|
- if char == 'x':
|
|
|
+ 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()
|
|
|
- self.update_drilldown()
|
|
|
- sleeptime = DELAY_INITIAL
|
|
|
- if char == 'q':
|
|
|
- break
|
|
|
if char == 'c':
|
|
|
self.stats.fields_filter = DEFAULT_REGEX
|
|
|
self.refresh_header(0)
|
|
|
self.update_pid(0)
|
|
|
- sleeptime = DELAY_INITIAL
|
|
|
if char == 'f':
|
|
|
+ curses.curs_set(1)
|
|
|
self.show_filter_selection()
|
|
|
- sleeptime = DELAY_INITIAL
|
|
|
+ curses.curs_set(0)
|
|
|
+ sleeptime = self._delay_initial
|
|
|
if char == 'g':
|
|
|
+ curses.curs_set(1)
|
|
|
self.show_vm_selection_by_guest_name()
|
|
|
- sleeptime = DELAY_INITIAL
|
|
|
+ curses.curs_set(0)
|
|
|
+ sleeptime = self._delay_initial
|
|
|
+ if char == 'h':
|
|
|
+ self.show_help_interactive()
|
|
|
+ if char == 'o':
|
|
|
+ self._sorting = not self._sorting
|
|
|
if char == 'p':
|
|
|
+ curses.curs_set(1)
|
|
|
self.show_vm_selection_by_pid()
|
|
|
- sleeptime = DELAY_INITIAL
|
|
|
+ curses.curs_set(0)
|
|
|
+ sleeptime = self._delay_initial
|
|
|
+ if char == 'q':
|
|
|
+ break
|
|
|
if char == 'r':
|
|
|
- self.refresh_header()
|
|
|
self.stats.reset()
|
|
|
- sleeptime = DELAY_INITIAL
|
|
|
+ if char == 's':
|
|
|
+ curses.curs_set(1)
|
|
|
+ self.show_set_update_interval()
|
|
|
+ curses.curs_set(0)
|
|
|
+ sleeptime = self._delay_initial
|
|
|
+ if char == 'x':
|
|
|
+ self.update_drilldown()
|
|
|
+ # prevents display of current values on next refresh
|
|
|
+ self.stats.get()
|
|
|
except KeyboardInterrupt:
|
|
|
break
|
|
|
except curses.error:
|
|
@@ -1227,13 +1422,17 @@ Requirements:
|
|
|
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
|
|
|
- x toggle reporting of stats for individual child trace events
|
|
|
r reset stats
|
|
|
+ s set update interval
|
|
|
+ x toggle reporting of stats for individual child trace events
|
|
|
Press any other key to refresh statistics immediately.
|
|
|
"""
|
|
|
|
|
@@ -1246,7 +1445,7 @@ Press any other key to refresh statistics immediately.
|
|
|
|
|
|
def cb_guest_to_pid(option, opt, val, parser):
|
|
|
try:
|
|
|
- pids = get_pid_from_gname(val)
|
|
|
+ pids = Tui.get_pid_from_gname(val)
|
|
|
except:
|
|
|
raise optparse.OptionValueError('Error while searching for guest '
|
|
|
'"{}", use "-p" to specify a pid '
|
|
@@ -1268,6 +1467,13 @@ Press any other key to refresh statistics immediately.
|
|
|
dest='once',
|
|
|
help='run in batch mode for one second',
|
|
|
)
|
|
|
+ optparser.add_option('-i', '--debugfs-include-past',
|
|
|
+ action='store_true',
|
|
|
+ default=False,
|
|
|
+ dest='dbgfs_include_past',
|
|
|
+ help='include all available data on past events for '
|
|
|
+ 'debugfs',
|
|
|
+ )
|
|
|
optparser.add_option('-l', '--log',
|
|
|
action='store_true',
|
|
|
default=False,
|
|
@@ -1288,7 +1494,7 @@ Press any other key to refresh statistics immediately.
|
|
|
)
|
|
|
optparser.add_option('-f', '--fields',
|
|
|
action='store',
|
|
|
- default=None,
|
|
|
+ default=DEFAULT_REGEX,
|
|
|
dest='fields',
|
|
|
help='fields to display (regex)',
|
|
|
)
|
|
@@ -1311,20 +1517,6 @@ Press any other key to refresh statistics immediately.
|
|
|
return options
|
|
|
|
|
|
|
|
|
-def get_providers(options):
|
|
|
- """Returns a list of data providers depending on the passed options."""
|
|
|
- providers = []
|
|
|
-
|
|
|
- if options.tracepoints:
|
|
|
- providers.append(TracepointProvider())
|
|
|
- if options.debugfs:
|
|
|
- providers.append(DebugfsProvider())
|
|
|
- if len(providers) == 0:
|
|
|
- providers.append(TracepointProvider())
|
|
|
-
|
|
|
- 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'):
|
|
@@ -1365,8 +1557,7 @@ def main():
|
|
|
sys.stderr.write('Did you use a (unsupported) tid instead of a pid?\n')
|
|
|
sys.exit('Specified pid does not exist.')
|
|
|
|
|
|
- providers = get_providers(options)
|
|
|
- stats = Stats(providers, options.pid, fields=options.fields)
|
|
|
+ stats = Stats(options)
|
|
|
|
|
|
if options.log:
|
|
|
log(stats)
|