Skip to content

Commit

Permalink
tools/kvm_stat: separate drilldown and fields filtering
Browse files Browse the repository at this point in the history
Drilldown (i.e. toggle display of child trace events) was implemented by
overriding the fields filter. This resulted in inconsistencies: E.g. when
drilldown was not active, adding a filter that also matches child trace
events would not only filter fields according to the filter, but also add
in the child trace events matching the filter. E.g. on x86, setting
'kvm_userspace_exit' as the fields filter after startup would result in
display of kvm_userspace_exit(DCR), although that wasn't previously
present - not exactly what one would expect from a filter.
This patch addresses the issue by keeping drilldown and fields filter
separate. While at it, we also fix a PEP8 issue by adding a blank line
at one place (since we're in the area...).
We implement this by adding a framework that also allows to define a
taxonomy among the debugfs events to identify child trace events. I.e.
drilldown using 'x' can now also work with debugfs. A respective parent-
child relationship is only known for S390 at the moment, but could be
added adjusting other platforms' ARCH.dbg_is_child() methods
accordingly.

Signed-off-by: Stefan Raspl <raspl@linux.vnet.ibm.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
  • Loading branch information
Stefan-Raspl authored and bonzini committed Feb 24, 2018
1 parent 516f119 commit 18e8f41
Showing 1 changed file with 100 additions and 43 deletions.
143 changes: 100 additions & 43 deletions tools/kvm/kvm_stat/kvm_stat
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ IOCTL_NUMBERS = {
}

ENCODING = locale.getpreferredencoding(False)
TRACE_FILTER = re.compile(r'^[^\(]*$')


class Arch(object):
Expand Down Expand Up @@ -260,13 +261,22 @@ class Arch(object):
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_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):
Expand All @@ -282,20 +292,34 @@ class ArchPPC(Arch):
self.ioctl_numbers['SET_FILTER'] = 0x80002406 | char_ptr_size << 16
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_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_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()


Expand Down Expand Up @@ -472,6 +496,10 @@ class Event(object):

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."""
Expand Down Expand Up @@ -499,7 +527,7 @@ class TracepointProvider(Provider):
self.group_leaders = []
self.filters = self._get_filters()
self.update_fields(fields_filter)
self.pid = pid
super(TracepointProvider, self).__init__(pid)

@staticmethod
def _get_filters():
Expand All @@ -519,7 +547,7 @@ class TracepointProvider(Provider):
return filters

def _get_available_fields(self):
"""Returns a list of available event's of format 'event name(filter
"""Returns a list of available events of format 'event name(filter
name)'.
All available events have directories under
Expand Down Expand Up @@ -547,7 +575,8 @@ class TracepointProvider(Provider):
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)]
if self.is_field_wanted(fields_filter, field) or
ARCH.tracepoint_is_child(field)]

@staticmethod
def _get_online_cpus():
Expand Down Expand Up @@ -668,8 +697,12 @@ class TracepointProvider(Provider):
ret = defaultdict(int)
for group in self.group_leaders:
for name, val in group.read().items():
if name in self._fields:
ret[name] += val
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):
Expand All @@ -687,7 +720,7 @@ class DebugfsProvider(Provider):
self._baseline = {}
self.do_read = True
self.paths = []
self.pid = pid
super(DebugfsProvider, self).__init__(pid)
if include_past:
self._restore()

Expand All @@ -702,7 +735,8 @@ class DebugfsProvider(Provider):
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)]
if self.is_field_wanted(fields_filter, field) or
ARCH.debugfs_is_child(field)]

@property
def fields(self):
Expand Down Expand Up @@ -763,14 +797,15 @@ class DebugfsProvider(Provider):
self._baseline[key] = 0
if self._baseline.get(key, -1) == -1:
self._baseline[key] = value
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
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

Expand Down Expand Up @@ -812,6 +847,7 @@ class Stats(object):
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."""
Expand Down Expand Up @@ -860,12 +896,29 @@ class Stats(object):
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."""
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 if by_guest else provider.fields:
for key in new:
oldval = self.values.get(key, EventStat(0, 0)).value
newval = new.get(key, 0)
newdelta = newval - oldval
Expand Down Expand Up @@ -898,10 +951,10 @@ class Stats(object):
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


Expand Down Expand Up @@ -1031,14 +1084,6 @@ class Tui(object):

return name

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 = DEFAULT_REGEX

elif self.stats.fields_filter == DEFAULT_REGEX:
self.stats.fields_filter = None

def _update_pid(self, pid):
"""Propagates pid selection to stats object."""
self.screen.addstr(4, 1, 'Updating pid filter...')
Expand All @@ -1060,8 +1105,7 @@ class Tui(object):
.format(pid, gname), curses.A_BOLD)
else:
self.screen.addstr(0, 0, 'kvm statistics - summary', curses.A_BOLD)
if self.stats.fields_filter and self.stats.fields_filter \
!= DEFAULT_REGEX:
if self.stats.fields_filter:
regex = self.stats.fields_filter
if len(regex) > MAX_REGEX_LEN:
regex = regex[:MAX_REGEX_LEN] + '...'
Expand All @@ -1077,14 +1121,21 @@ class Tui(object):
self.screen.refresh()

def _refresh_body(self, sleeptime):
def is_child_field(field):
return field.find('(') != -1

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 key.find('(') == -1:
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
Expand All @@ -1101,19 +1152,26 @@ class Tui(object):
# sort by overall value
return v.value

sorted_items = sorted(stats.items(), key=sortkey, reverse=True)

# print events
tavg = 0
for key, values in sorted(stats.items(), key=sortkey, reverse=True):
for key, values in sorted_items:
if row >= self.screen.getmaxyx()[0] - 1:
break
if not values.value and not values.delta:
break
if values == (0, 0):
continue
if not self.stats.child_events and key.find(' ') != -1:
continue
if values.value is not None:
cur = int(round(values.delta / sleeptime)) if values.delta else ''
if self._display_guests:
key = self.get_gname_from_pid(key)
self.screen.addstr(row, 1, '%-40s %10d%7.1f %8s' %
(key, values.value, values.value * 100 / total,
cur))
if not key:
continue
self.screen.addstr(row, 1, '%-40s %10d%7.1f %8s' % (key
.split(' ')[0], values.value,
values.value * 100 / total, cur))
if cur != '' and key.find('(') == -1:
tavg += cur
row += 1
Expand Down Expand Up @@ -1189,7 +1247,7 @@ class Tui(object):
regex = self.screen.getstr().decode(ENCODING)
curses.noecho()
if len(regex) == 0:
self.stats.fields_filter = DEFAULT_REGEX
self.stats.fields_filter = ''
self._refresh_header()
return
try:
Expand Down Expand Up @@ -1307,7 +1365,7 @@ class Tui(object):
self._display_guests = not self._display_guests
self._refresh_header()
if char == 'c':
self.stats.fields_filter = DEFAULT_REGEX
self.stats.fields_filter = ''
self._refresh_header(0)
self._update_pid(0)
if char == 'f':
Expand All @@ -1332,9 +1390,7 @@ class Tui(object):
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(self._display_guests)
self.stats.child_events = not self.stats.child_events
except KeyboardInterrupt:
break
except curses.error:
Expand All @@ -1348,7 +1404,8 @@ def batch(stats):
time.sleep(1)
s = stats.get()
for key, values in sorted(s.items()):
print('%-42s%10d%10d' % (key, values.value, values.delta))
print('%-42s%10d%10d' % (key.split(' ')[0], values.value,
values.delta))
except KeyboardInterrupt:
pass

Expand All @@ -1359,7 +1416,7 @@ def log(stats):

def banner():
for key in keys:
print(key, end=' ')
print(key.split(' ')[0], end=' ')
print()

def statline():
Expand Down Expand Up @@ -1470,7 +1527,7 @@ Press any other key to refresh statistics immediately.
)
optparser.add_option('-f', '--fields',
action='store',
default=DEFAULT_REGEX,
default='',
dest='fields',
help='''fields to display (regex)
"-f help" for a list of available events''',
Expand Down

0 comments on commit 18e8f41

Please sign in to comment.