2 # SPDX-License-Identifier: GPL-2.0-only
4 # top-like utility for displaying kvm statistics
6 # Copyright 2006-2008 Qumranet Technologies
7 # Copyright 2008-2011 Red Hat, Inc.
12 """The kvm_stat module outputs statistics about running KVM VMs
14 Three different ways of output formatting are available:
15 - as a top-like text ui
16 - in a key -> value format
17 - in an all keys, all values format
19 The data is sampled from the KVM's debugfs entries and its perf events.
21 from __future__ import print_function
36 from collections import defaultdict, namedtuple
37 from functools import reduce
38 from datetime import datetime
42 'EXTERNAL_INTERRUPT': 1,
44 'PENDING_INTERRUPT': 7,
68 'MWAIT_INSTRUCTION': 36,
69 'MONITOR_INSTRUCTION': 39,
70 'PAUSE_INSTRUCTION': 40,
71 'MCE_DURING_VMENTRY': 41,
72 'TPR_BELOW_THRESHOLD': 43,
113 'CR0_SEL_WRITE': 0x065,
137 'TASK_SWITCH': 0x07d,
138 'FERR_FREEZE': 0x07e,
157 # EC definition of HSR (from arch/arm64/include/asm/kvm_arm.h)
158 AARCH64_EXIT_REASONS = {
196 # From include/uapi/linux/kvm.h, KVM_EXIT_xxx
197 USERSPACE_EXIT_REASONS = {
205 'IRQ_WINDOW_OPEN': 7,
215 'INTERNAL_ERROR': 17,
226 'SET_FILTER': 0x40082406,
227 'ENABLE': 0x00002400,
228 'DISABLE': 0x00002401,
232 signal_received = False
234 ENCODING = locale.getpreferredencoding(False)
235 TRACE_FILTER = re.compile(r'^[^\(]*$')
239 """Encapsulates global architecture specific data.
241 Contains the performance event open syscall and ioctl numbers, as
242 well as the VM exit reasons for the architecture it runs on.
247 machine = os.uname()[4]
249 if machine.startswith('ppc'):
251 elif machine.startswith('aarch64'):
253 elif machine.startswith('s390'):
257 for line in open('/proc/cpuinfo'):
258 if not line.startswith('flags'):
263 return ArchX86(VMX_EXIT_REASONS)
265 return ArchX86(SVM_EXIT_REASONS)
268 def tracepoint_is_child(self, field):
269 if (TRACE_FILTER.match(field)):
271 return field.split('(', 1)[0]
275 def __init__(self, exit_reasons):
276 self.sc_perf_evt_open = 298
277 self.ioctl_numbers = IOCTL_NUMBERS
278 self.exit_reason_field = 'exit_reason'
279 self.exit_reasons = exit_reasons
281 def debugfs_is_child(self, field):
282 """ Returns name of parent if 'field' is a child, None otherwise """
288 self.sc_perf_evt_open = 319
289 self.ioctl_numbers = IOCTL_NUMBERS
290 self.ioctl_numbers['ENABLE'] = 0x20002400
291 self.ioctl_numbers['DISABLE'] = 0x20002401
292 self.ioctl_numbers['RESET'] = 0x20002403
294 # PPC comes in 32 and 64 bit and some generated ioctl
295 # numbers depend on the wordsize.
296 char_ptr_size = ctypes.sizeof(ctypes.c_char_p)
297 self.ioctl_numbers['SET_FILTER'] = 0x80002406 | char_ptr_size << 16
298 self.exit_reason_field = 'exit_nr'
299 self.exit_reasons = {}
301 def debugfs_is_child(self, field):
302 """ Returns name of parent if 'field' is a child, None otherwise """
308 self.sc_perf_evt_open = 241
309 self.ioctl_numbers = IOCTL_NUMBERS
310 self.exit_reason_field = 'esr_ec'
311 self.exit_reasons = AARCH64_EXIT_REASONS
313 def debugfs_is_child(self, field):
314 """ Returns name of parent if 'field' is a child, None otherwise """
318 class ArchS390(Arch):
320 self.sc_perf_evt_open = 331
321 self.ioctl_numbers = IOCTL_NUMBERS
322 self.exit_reason_field = None
323 self.exit_reasons = None
325 def debugfs_is_child(self, field):
326 """ Returns name of parent if 'field' is a child, None otherwise """
327 if field.startswith('instruction_'):
328 return 'exit_instruction'
331 ARCH = Arch.get_arch()
334 class perf_event_attr(ctypes.Structure):
335 """Struct that holds the necessary data to set up a trace event.
337 For an extensive explanation see perf_event_open(2) and
338 include/uapi/linux/perf_event.h, struct perf_event_attr
340 All fields that are not initialized in the constructor are 0.
343 _fields_ = [('type', ctypes.c_uint32),
344 ('size', ctypes.c_uint32),
345 ('config', ctypes.c_uint64),
346 ('sample_freq', ctypes.c_uint64),
347 ('sample_type', ctypes.c_uint64),
348 ('read_format', ctypes.c_uint64),
349 ('flags', ctypes.c_uint64),
350 ('wakeup_events', ctypes.c_uint32),
351 ('bp_type', ctypes.c_uint32),
352 ('bp_addr', ctypes.c_uint64),
353 ('bp_len', ctypes.c_uint64),
357 super(self.__class__, self).__init__()
358 self.type = PERF_TYPE_TRACEPOINT
359 self.size = ctypes.sizeof(self)
360 self.read_format = PERF_FORMAT_GROUP
363 PERF_TYPE_TRACEPOINT = 2
364 PERF_FORMAT_GROUP = 1 << 3
368 """Represents a perf event group."""
373 def add_event(self, event):
374 self.events.append(event)
377 """Returns a dict with 'event name: value' for all events in the
380 Values are read by reading from the file descriptor of the
381 event that is the group leader. See perf_event_open(2) for
384 Read format for the used event configuration is:
386 u64 nr; /* The number of events */
388 u64 value; /* The value of the event */
393 length = 8 * (1 + len(self.events))
394 read_format = 'xxxxxxxx' + 'Q' * len(self.events)
395 return dict(zip([event.name for event in self.events],
396 struct.unpack(read_format,
397 os.read(self.events[0].fd, length))))
401 """Represents a performance event and manages its life cycle."""
402 def __init__(self, name, group, trace_cpu, trace_pid, trace_point,
403 trace_filter, trace_set='kvm'):
404 self.libc = ctypes.CDLL('libc.so.6', use_errno=True)
405 self.syscall = self.libc.syscall
408 self._setup_event(group, trace_cpu, trace_pid, trace_point,
409 trace_filter, trace_set)
412 """Closes the event's file descriptor.
414 As no python file object was created for the file descriptor,
415 python will not reference count the descriptor and will not
416 close it itself automatically, so we do it.
422 def _perf_event_open(self, attr, pid, cpu, group_fd, flags):
423 """Wrapper for the sys_perf_evt_open() syscall.
425 Used to set up performance events, returns a file descriptor or -1
430 - struct perf_event_attr *
431 - pid or -1 to monitor all pids
432 - cpu number or -1 to monitor all cpus
433 - The file descriptor of the group leader or -1 to create a group.
437 return self.syscall(ARCH.sc_perf_evt_open, ctypes.pointer(attr),
438 ctypes.c_int(pid), ctypes.c_int(cpu),
439 ctypes.c_int(group_fd), ctypes.c_long(flags))
441 def _setup_event_attribute(self, trace_set, trace_point):
442 """Returns an initialized ctype perf_event_attr struct."""
444 id_path = os.path.join(PATH_DEBUGFS_TRACING, 'events', trace_set,
447 event_attr = perf_event_attr()
448 event_attr.config = int(open(id_path).read())
451 def _setup_event(self, group, trace_cpu, trace_pid, trace_point,
452 trace_filter, trace_set):
453 """Sets up the perf event in Linux.
455 Issues the syscall to register the event in the kernel and
456 then sets the optional filter.
460 event_attr = self._setup_event_attribute(trace_set, trace_point)
462 # First event will be group leader.
465 # All others have to pass the leader's descriptor instead.
467 group_leader = group.events[0].fd
469 fd = self._perf_event_open(event_attr, trace_pid,
470 trace_cpu, group_leader, 0)
472 err = ctypes.get_errno()
473 raise OSError(err, os.strerror(err),
474 'while calling sys_perf_event_open().')
477 fcntl.ioctl(fd, ARCH.ioctl_numbers['SET_FILTER'],
483 """Enables the trace event in the kernel.
485 Enabling the group leader makes reading counters from it and the
486 events under it possible.
489 fcntl.ioctl(self.fd, ARCH.ioctl_numbers['ENABLE'], 0)
492 """Disables the trace event in the kernel.
494 Disabling the group leader makes reading all counters under it
498 fcntl.ioctl(self.fd, ARCH.ioctl_numbers['DISABLE'], 0)
501 """Resets the count of the trace event in the kernel."""
502 fcntl.ioctl(self.fd, ARCH.ioctl_numbers['RESET'], 0)
505 class Provider(object):
506 """Encapsulates functionalities used by all providers."""
507 def __init__(self, pid):
508 self.child_events = False
512 def is_field_wanted(fields_filter, field):
513 """Indicate whether field is valid according to fields_filter."""
514 if not fields_filter:
516 return re.match(fields_filter, field) is not None
520 """Returns os.walk() data for specified directory.
522 As it is only a wrapper it returns the same 3-tuple of (dirpath,
523 dirnames, filenames).
525 return next(os.walk(path))
528 class TracepointProvider(Provider):
529 """Data provider for the stats class.
531 Manages the events/groups from which it acquires its data.
534 def __init__(self, pid, fields_filter):
535 self.group_leaders = []
536 self.filters = self._get_filters()
537 self.update_fields(fields_filter)
538 super(TracepointProvider, self).__init__(pid)
542 """Returns a dict of trace events, their filter ids and
543 the values that can be filtered.
545 Trace events can be filtered for special values by setting a
546 filter string via an ioctl. The string normally has the format
547 identifier==value. For each filter a new event will be created, to
548 be able to distinguish the events.
552 filters['kvm_userspace_exit'] = ('reason', USERSPACE_EXIT_REASONS)
553 if ARCH.exit_reason_field and ARCH.exit_reasons:
554 filters['kvm_exit'] = (ARCH.exit_reason_field, ARCH.exit_reasons)
557 def _get_available_fields(self):
558 """Returns a list of available events of format 'event name(filter
561 All available events have directories under
562 /sys/kernel/debug/tracing/events/ which export information
563 about the specific event. Therefore, listing the dirs gives us
564 a list of all available events.
566 Some events like the vm exit reasons can be filtered for
567 specific values. To take account for that, the routine below
568 creates special fields with the following format:
569 event name(filter name)
572 path = os.path.join(PATH_DEBUGFS_TRACING, 'events', 'kvm')
573 fields = self.walkdir(path)[1]
576 if field in self.filters:
577 filter_name_, filter_dicts = self.filters[field]
578 for name in filter_dicts:
579 extra.append(field + '(' + name + ')')
583 def update_fields(self, fields_filter):
584 """Refresh fields, applying fields_filter"""
585 self.fields = [field for field in self._get_available_fields()
586 if self.is_field_wanted(fields_filter, field)]
587 # add parents for child fields - otherwise we won't see any output!
588 for field in self._fields:
589 parent = ARCH.tracepoint_is_child(field)
590 if (parent and parent not in self._fields):
591 self.fields.append(parent)
594 def _get_online_cpus():
595 """Returns a list of cpu id integers."""
596 def parse_int_list(list_string):
597 """Returns an int list from a string of comma separated integers and
600 members = list_string.split(',')
602 for member in members:
603 if '-' not in member:
604 integers.append(int(member))
606 int_range = member.split('-')
607 integers.extend(range(int(int_range[0]),
608 int(int_range[1]) + 1))
612 with open('/sys/devices/system/cpu/online') as cpu_list:
613 cpu_string = cpu_list.readline()
614 return parse_int_list(cpu_string)
616 def _setup_traces(self):
617 """Creates all event and group objects needed to be able to retrieve
619 fields = self._get_available_fields()
621 # Fetch list of all threads of the monitored pid, as qemu
622 # starts a thread for each vcpu.
623 path = os.path.join('/proc', str(self._pid), 'task')
624 groupids = self.walkdir(path)[1]
626 groupids = self._get_online_cpus()
628 # The constant is needed as a buffer for python libs, std
629 # streams and other files that the script opens.
630 newlim = len(groupids) * len(fields) + 50
632 softlim_, hardlim = resource.getrlimit(resource.RLIMIT_NOFILE)
635 # Now we need CAP_SYS_RESOURCE, to increase the hard limit.
636 resource.setrlimit(resource.RLIMIT_NOFILE, (newlim, newlim))
638 # Raising the soft limit is sufficient.
639 resource.setrlimit(resource.RLIMIT_NOFILE, (newlim, hardlim))
642 sys.exit("NOFILE rlimit could not be raised to {0}".format(newlim))
644 for groupid in groupids:
649 match = re.match(r'(.*)\((.*)\)', name)
651 tracepoint, sub = match.groups()
652 tracefilter = ('%s==%d\0' %
653 (self.filters[tracepoint][0],
654 self.filters[tracepoint][1][sub]))
656 # From perf_event_open(2):
657 # pid > 0 and cpu == -1
658 # This measures the specified process/thread on any CPU.
660 # pid == -1 and cpu >= 0
661 # This measures all processes/threads on the specified CPU.
662 trace_cpu = groupid if self._pid == 0 else -1
663 trace_pid = int(groupid) if self._pid != 0 else -1
665 group.add_event(Event(name=name,
669 trace_point=tracepoint,
670 trace_filter=tracefilter))
672 self.group_leaders.append(group)
679 def fields(self, fields):
680 """Enables/disables the (un)wanted events"""
681 self._fields = fields
682 for group in self.group_leaders:
683 for index, event in enumerate(group.events):
684 if event.name in fields:
688 # Do not disable the group leader.
689 # It would disable all of its events.
699 """Changes the monitored pid by setting new traces."""
701 # The garbage collector will get rid of all Event/Group
702 # objects and open files after removing the references.
703 self.group_leaders = []
705 self.fields = self._fields
707 def read(self, by_guest=0):
708 """Returns 'event name: current value' for all enabled events."""
709 ret = defaultdict(int)
710 for group in self.group_leaders:
711 for name, val in group.read().items():
712 if name not in self._fields:
714 parent = ARCH.tracepoint_is_child(name)
721 """Reset all field counters"""
722 for group in self.group_leaders:
723 for event in group.events:
727 class DebugfsProvider(Provider):
728 """Provides data from the files that KVM creates in the kvm debugfs
730 def __init__(self, pid, fields_filter, include_past):
731 self.update_fields(fields_filter)
735 super(DebugfsProvider, self).__init__(pid)
739 def _get_available_fields(self):
740 """"Returns a list of available fields.
742 The fields are all available KVM debugfs files
745 return self.walkdir(PATH_DEBUGFS_KVM)[2]
747 def update_fields(self, fields_filter):
748 """Refresh fields, applying fields_filter"""
749 self._fields = [field for field in self._get_available_fields()
750 if self.is_field_wanted(fields_filter, field)]
751 # add parents for child fields - otherwise we won't see any output!
752 for field in self._fields:
753 parent = ARCH.debugfs_is_child(field)
754 if (parent and parent not in self._fields):
755 self.fields.append(parent)
762 def fields(self, fields):
763 self._fields = fields
774 vms = self.walkdir(PATH_DEBUGFS_KVM)[1]
778 self.paths = list(filter(lambda x: "{}-".format(pid) in x, vms))
784 def _verify_paths(self):
785 """Remove invalid paths"""
786 for path in self.paths:
787 if not os.path.exists(os.path.join(PATH_DEBUGFS_KVM, path)):
788 self.paths.remove(path)
791 def read(self, reset=0, by_guest=0):
792 """Returns a dict with format:'file name / field -> current value'.
796 1 reset field counts to 0
797 2 restore the original field counts
802 # If no debugfs filtering support is available, then don't read.
810 for entry in os.walk(PATH_DEBUGFS_KVM):
814 for field in self._fields:
815 value = self._read_field(field, path)
818 self._baseline[key] = value
820 self._baseline[key] = 0
821 if self._baseline.get(key, -1) == -1:
822 self._baseline[key] = value
823 parent = ARCH.debugfs_is_child(field)
825 field = field + ' ' + parent
828 field = key.split('-')[0] # set 'field' to 'pid'
829 increment = value - self._baseline.get(key, 0)
831 results[field] += increment
833 results[field] = increment
837 def _read_field(self, field, path):
838 """Returns the value of a single field from a specific VM."""
840 return int(open(os.path.join(PATH_DEBUGFS_KVM,
848 """Reset field counters"""
853 """Reset field counters"""
858 EventStat = namedtuple('EventStat', ['value', 'delta'])
862 """Manages the data providers and the data they provide.
864 It is used to set filters on the provider's data and collect all
868 def __init__(self, options):
869 self.providers = self._get_providers(options)
870 self._pid_filter = options.pid
871 self._fields_filter = options.fields
873 self._child_events = False
875 def _get_providers(self, options):
876 """Returns a list of data providers depending on the passed options."""
880 providers.append(DebugfsProvider(options.pid, options.fields,
881 options.debugfs_include_past))
882 if options.tracepoints or not providers:
883 providers.append(TracepointProvider(options.pid, options.fields))
887 def _update_provider_filters(self):
888 """Propagates fields filters to providers."""
889 # As we reset the counters when updating the fields we can
890 # also clear the cache of old values.
892 for provider in self.providers:
893 provider.update_fields(self._fields_filter)
897 for provider in self.providers:
901 def fields_filter(self):
902 return self._fields_filter
904 @fields_filter.setter
905 def fields_filter(self, fields_filter):
906 if fields_filter != self._fields_filter:
907 self._fields_filter = fields_filter
908 self._update_provider_filters()
911 def pid_filter(self):
912 return self._pid_filter
915 def pid_filter(self, pid):
916 if pid != self._pid_filter:
917 self._pid_filter = pid
919 for provider in self.providers:
920 provider.pid = self._pid_filter
923 def child_events(self):
924 return self._child_events
927 def child_events(self, val):
928 self._child_events = val
929 for provider in self.providers:
930 provider.child_events = val
932 def get(self, by_guest=0):
933 """Returns a dict with field -> (value, delta to last value) of all
936 * plain: 'key' is event name
937 * child-parent: 'key' is in format '<child> <parent>'
938 * pid: 'key' is the pid of the guest, and the record contains the
939 aggregated event data
940 These formats are generated by the providers, and handled in class TUI.
942 for provider in self.providers:
943 new = provider.read(by_guest=by_guest)
945 oldval = self.values.get(key, EventStat(0, 0)).value
946 newval = new.get(key, 0)
947 newdelta = newval - oldval
948 self.values[key] = EventStat(newval, newdelta)
951 def toggle_display_guests(self, to_pid):
952 """Toggle between collection of stats by individual event and by
955 Events reported by DebugfsProvider change when switching to/from
956 reading by guest values. Hence we have to remove the excess event
957 names from self.values.
960 if any(isinstance(ins, TracepointProvider) for ins in self.providers):
963 for provider in self.providers:
964 if isinstance(provider, DebugfsProvider):
965 for key in provider.fields:
966 if key in self.values.keys():
969 oldvals = self.values.copy()
973 # Update oldval (see get())
979 MAX_GUEST_NAME_LEN = 48
987 """Instruments curses to draw a nice text ui."""
988 def __init__(self, stats, opts):
991 self._delay_initial = 0.25
992 self._delay_regular = opts.set_delay
993 self._sorting = SORT_DEFAULT
994 self._display_guests = 0
997 """Initialises curses for later use. Based on curses.wrapper
998 implementation from the Python standard library."""
999 self.screen = curses.initscr()
1003 # The try/catch works around a minor bit of
1004 # over-conscientiousness in the curses module, the error
1005 # return from C start_color() is ignorable.
1007 curses.start_color()
1008 except curses.error:
1011 # Hide cursor in extra statement as some monochrome terminals
1012 # might support hiding but not colors.
1015 except curses.error:
1018 curses.use_default_colors()
1021 def __exit__(self, *exception):
1022 """Resets the terminal to its normal state. Based on curses.wrapper
1023 implementation from the Python standard library."""
1025 self.screen.keypad(0)
1031 def get_all_gnames():
1032 """Returns a list of (pid, gname) tuples of all running guests"""
1035 child = subprocess.Popen(['ps', '-A', '--format', 'pid,args'],
1036 stdout=subprocess.PIPE)
1039 for line in child.stdout:
1040 line = line.decode(ENCODING).lstrip().split(' ', 1)
1041 # perform a sanity check before calling the more expensive
1042 # function to possibly extract the guest name
1043 if ' -name ' in line[1]:
1044 res.append((line[0], Tui.get_gname_from_pid(line[0])))
1045 child.stdout.close()
1049 def _print_all_gnames(self, row):
1050 """Print a list of all running guests along with their pids."""
1051 self.screen.addstr(row, 2, '%8s %-60s' %
1052 ('Pid', 'Guest Name (fuzzy list, might be '
1057 for line in self.get_all_gnames():
1058 self.screen.addstr(row, 2, '%8s %-60s' % (line[0], line[1]))
1060 if row >= self.screen.getmaxyx()[0]:
1063 self.screen.addstr(row + 1, 2, 'Not available')
1066 def get_pid_from_gname(gname):
1067 """Fuzzy function to convert guest name to QEMU process pid.
1069 Returns a list of potential pids, can be empty if no match found.
1070 Throws an exception on processing errors.
1074 for line in Tui.get_all_gnames():
1075 if gname == line[1]:
1076 pids.append(int(line[0]))
1081 def get_gname_from_pid(pid):
1082 """Returns the guest name for a QEMU process pid.
1084 Extracts the guest name from the QEMU comma line by processing the
1085 '-name' option. Will also handle names specified out of sequence.
1090 line = open('/proc/{}/cmdline'
1091 .format(pid), 'r').read().split('\0')
1092 parms = line[line.index('-name') + 1].split(',')
1094 # commas are escaped (i.e. ',,'), hence e.g. 'foo,bar' results
1095 # in # ['foo', '', 'bar'], which we revert here
1096 idx = parms.index('')
1097 parms[idx - 1] += ',' + parms[idx + 1]
1098 del parms[idx:idx+2]
1099 # the '-name' switch allows for two ways to specify the guest name,
1100 # where the plain name overrides the name specified via 'guest='
1105 if arg[:6] == 'guest=':
1107 except (ValueError, IOError, IndexError):
1112 def _update_pid(self, pid):
1113 """Propagates pid selection to stats object."""
1114 self.screen.addstr(4, 1, 'Updating pid filter...')
1115 self.screen.refresh()
1116 self.stats.pid_filter = pid
1118 def _refresh_header(self, pid=None):
1119 """Refreshes the header."""
1121 pid = self.stats.pid_filter
1123 gname = self.get_gname_from_pid(pid)
1126 gname = ('({})'.format(gname[:MAX_GUEST_NAME_LEN] + '...'
1127 if len(gname) > MAX_GUEST_NAME_LEN
1130 self._headline = 'kvm statistics - pid {0} {1}'.format(pid, gname)
1132 self._headline = 'kvm statistics - summary'
1133 self.screen.addstr(0, 0, self._headline, curses.A_BOLD)
1134 if self.stats.fields_filter:
1135 regex = self.stats.fields_filter
1136 if len(regex) > MAX_REGEX_LEN:
1137 regex = regex[:MAX_REGEX_LEN] + '...'
1138 self.screen.addstr(1, 17, 'regex filter: {0}'.format(regex))
1139 if self._display_guests:
1140 col_name = 'Guest Name'
1143 self.screen.addstr(2, 1, '%-40s %10s%7s %8s' %
1144 (col_name, 'Total', '%Total', 'CurAvg/s'),
1146 self.screen.addstr(4, 1, 'Collecting data...')
1147 self.screen.refresh()
1149 def _refresh_body(self, sleeptime):
1150 def insert_child(sorted_items, child, values, parent):
1151 num = len(sorted_items)
1152 for i in range(0, num):
1153 # only add child if parent is present
1154 if parent.startswith(sorted_items[i][0]):
1155 sorted_items.insert(i + 1, (' ' + child, values))
1157 def get_sorted_events(self, stats):
1158 """ separate parent and child events """
1159 if self._sorting == SORT_DEFAULT:
1161 # sort by (delta value, overall value)
1163 return (v.delta, v.value)
1166 # sort by overall value
1172 # we can't rule out child events to appear prior to parents even
1173 # when sorted - separate out all children first, and add in later
1174 for key, values in sorted(stats.items(), key=sortkey,
1176 if values == (0, 0):
1178 if key.find(' ') != -1:
1179 if not self.stats.child_events:
1181 childs.insert(0, (key, values))
1183 sorted_items.append((key, values))
1184 if self.stats.child_events:
1185 for key, values in childs:
1186 (child, parent) = key.split(' ')
1187 insert_child(sorted_items, child, values, parent)
1191 if not self._is_running_guest(self.stats.pid_filter):
1193 try: # ...to identify the guest by name in case it's back
1194 pids = self.get_pid_from_gname(self._gname)
1196 self._refresh_header(pids[0])
1197 self._update_pid(pids[0])
1201 self._display_guest_dead()
1202 # leave final data on screen
1205 self.screen.move(row, 0)
1206 self.screen.clrtobot()
1207 stats = self.stats.get(self._display_guests)
1210 for key, values in stats.items():
1211 if self._display_guests:
1212 if self.get_gname_from_pid(key):
1213 total += values.value
1215 if not key.find(' ') != -1:
1216 total += values.value
1218 ctotal += values.value
1220 # we don't have any fields, or all non-child events are filtered
1226 guest_removed = False
1227 for key, values in get_sorted_events(self, stats):
1228 if row >= self.screen.getmaxyx()[0] - 1 or values == (0, 0):
1230 if self._display_guests:
1231 key = self.get_gname_from_pid(key)
1234 cur = int(round(values.delta / sleeptime)) if values.delta else 0
1236 guest_removed = True
1240 tcur += values.delta
1241 ptotal = values.value
1245 self.screen.addstr(row, 1, '%-40s %10d%7.1f %8s' % (key,
1247 values.value * 100 / float(ltotal), cur))
1251 self.screen.addstr(4, 1, 'Guest removed, updating...')
1253 self.screen.addstr(4, 1, 'No matching events reported yet')
1255 tavg = int(round(tcur / sleeptime)) if tcur > 0 else ''
1256 self.screen.addstr(row, 1, '%-40s %10d %8s' %
1257 ('Total', total, tavg), curses.A_BOLD)
1258 self.screen.refresh()
1260 def _display_guest_dead(self):
1261 marker = ' Guest is DEAD '
1262 y = min(len(self._headline), 80 - len(marker))
1263 self.screen.addstr(0, y, marker, curses.A_BLINK | curses.A_STANDOUT)
1265 def _show_msg(self, text):
1266 """Display message centered text and exit on key press"""
1267 hint = 'Press any key to continue'
1270 (x, term_width) = self.screen.getmaxyx()
1273 start = (term_width - len(line)) // 2
1274 self.screen.addstr(row, start, line)
1276 self.screen.addstr(row + 1, (term_width - len(hint)) // 2, hint,
1278 self.screen.getkey()
1280 def _show_help_interactive(self):
1281 """Display help with list of interactive commands"""
1282 msg = (' b toggle events by guests (debugfs only, honors'
1285 ' f filter by regular expression',
1286 ' g filter by guest name/PID',
1287 ' h display interactive commands reference',
1288 ' o toggle sorting order (Total vs CurAvg/s)',
1289 ' p filter by guest name/PID',
1292 ' s set delay between refreshs (value range: '
1293 '%s-%s secs)' % (MIN_DELAY, MAX_DELAY),
1294 ' x toggle reporting of stats for individual child trace'
1296 'Any other key refreshes statistics immediately')
1299 self.screen.addstr(0, 0, "Interactive commands reference",
1301 self.screen.addstr(2, 0, "Press any key to exit", curses.A_STANDOUT)
1304 self.screen.addstr(row, 0, line)
1306 self.screen.getkey()
1307 self._refresh_header()
1309 def _show_filter_selection(self):
1310 """Draws filter selection mask.
1312 Asks for a valid regex and sets the fields filter accordingly.
1318 self.screen.addstr(0, 0,
1319 "Show statistics for events matching a regex.",
1321 self.screen.addstr(2, 0,
1322 "Current regex: {0}"
1323 .format(self.stats.fields_filter))
1324 self.screen.addstr(5, 0, msg)
1325 self.screen.addstr(3, 0, "New regex: ")
1327 regex = self.screen.getstr().decode(ENCODING)
1330 self.stats.fields_filter = ''
1331 self._refresh_header()
1335 self.stats.fields_filter = regex
1336 self._refresh_header()
1339 msg = '"' + regex + '": Not a valid regular expression'
1342 def _show_set_update_interval(self):
1343 """Draws update interval selection mask."""
1347 self.screen.addstr(0, 0, 'Set update interval (defaults to %.1fs).'
1348 % DELAY_DEFAULT, curses.A_BOLD)
1349 self.screen.addstr(4, 0, msg)
1350 self.screen.addstr(2, 0, 'Change delay from %.1fs to ' %
1351 self._delay_regular)
1353 val = self.screen.getstr().decode(ENCODING)
1359 err = is_delay_valid(delay)
1364 delay = DELAY_DEFAULT
1365 self._delay_regular = delay
1369 msg = '"' + str(val) + '": Invalid value'
1370 self._refresh_header()
1372 def _is_running_guest(self, pid):
1373 """Check if pid is still a running process."""
1376 return os.path.isdir(os.path.join('/proc/', str(pid)))
1378 def _show_vm_selection_by_guest(self):
1379 """Draws guest selection mask.
1381 Asks for a guest name or pid until a valid guest name or '' is entered.
1387 self.screen.addstr(0, 0,
1388 'Show statistics for specific guest or pid.',
1390 self.screen.addstr(1, 0,
1391 'This might limit the shown data to the trace '
1393 self.screen.addstr(5, 0, msg)
1394 self._print_all_gnames(7)
1397 self.screen.addstr(3, 0, "Guest or pid [ENTER exits]: ")
1398 guest = self.screen.getstr().decode(ENCODING)
1402 if not guest or guest == '0':
1405 if not self._is_running_guest(guest):
1406 msg = '"' + guest + '": Not a running process'
1412 pids = self.get_pid_from_gname(guest)
1414 msg = '"' + guest + '": Internal error while searching, ' \
1415 'use pid filter instead'
1418 msg = '"' + guest + '": Not an active guest'
1421 msg = '"' + guest + '": Multiple matches found, use pid ' \
1427 self._refresh_header(pid)
1428 self._update_pid(pid)
1430 def show_stats(self):
1431 """Refreshes the screen and processes user input."""
1432 sleeptime = self._delay_initial
1433 self._refresh_header()
1434 start = 0.0 # result based on init value never appears on screen
1436 self._refresh_body(time.time() - start)
1437 curses.halfdelay(int(sleeptime * 10))
1439 sleeptime = self._delay_regular
1441 char = self.screen.getkey()
1443 self._display_guests = not self._display_guests
1444 if self.stats.toggle_display_guests(self._display_guests):
1445 self._show_msg(['Command not available with '
1446 'tracepoints enabled', 'Restart with '
1447 'debugfs only (see option \'-d\') and '
1449 self._display_guests = not self._display_guests
1450 self._refresh_header()
1452 self.stats.fields_filter = ''
1453 self._refresh_header(0)
1457 self._show_filter_selection()
1459 sleeptime = self._delay_initial
1460 if char == 'g' or char == 'p':
1461 self._show_vm_selection_by_guest()
1462 sleeptime = self._delay_initial
1464 self._show_help_interactive()
1466 self._sorting = not self._sorting
1473 self._show_set_update_interval()
1475 sleeptime = self._delay_initial
1477 self.stats.child_events = not self.stats.child_events
1478 except KeyboardInterrupt:
1480 except curses.error:
1485 """Prints statistics in a key, value format."""
1490 for key, values in sorted(s.items()):
1491 print('%-42s%10d%10d' % (key.split(' ')[0], values.value,
1493 except KeyboardInterrupt:
1497 class StdFormat(object):
1498 def __init__(self, keys):
1501 self._banner += key.split(' ')[0] + ' '
1503 def get_banner(self):
1506 def get_statline(self, keys, s):
1509 res += ' %9d' % s[key].delta
1513 class CSVFormat(object):
1514 def __init__(self, keys):
1515 self._banner = 'timestamp'
1516 self._banner += reduce(lambda res, key: "{},{!s}".format(res,
1517 key.split(' ')[0]), keys, '')
1519 def get_banner(self):
1522 def get_statline(self, keys, s):
1523 return reduce(lambda res, key: "{},{!s}".format(res, s[key].delta),
1527 def log(stats, opts, frmt, keys):
1528 """Prints statistics as reiterating key block, multiple value blocks."""
1529 global signal_received
1534 def do_banner(opts):
1536 if opts.log_to_file:
1539 f = open(opts.log_to_file, 'a')
1540 except (IOError, OSError):
1541 sys.exit("Error: Could not open file: %s" %
1543 if isinstance(frmt, CSVFormat) and f.tell() != 0:
1545 print(frmt.get_banner(), file=f or sys.stdout)
1547 def do_statline(opts, values):
1548 statline = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + \
1549 frmt.get_statline(keys, values)
1550 print(statline, file=f or sys.stdout)
1553 banner_printed = True
1556 time.sleep(opts.set_delay)
1558 banner_printed = True
1562 signal_received = False
1563 if (line % banner_repeat == 0 and not banner_printed and
1564 not (opts.log_to_file and isinstance(frmt, CSVFormat))):
1566 banner_printed = True
1567 values = stats.get()
1568 if (not opts.skip_zero_records or
1569 any(values[k].delta != 0 for k in keys)):
1570 do_statline(opts, values)
1572 banner_printed = False
1573 except KeyboardInterrupt:
1576 if opts.log_to_file:
1580 def handle_signal(sig, frame):
1581 global signal_received
1583 signal_received = True
1588 def is_delay_valid(delay):
1589 """Verify delay is in valid value range."""
1591 if delay < MIN_DELAY:
1592 msg = '"' + str(delay) + '": Delay must be >=%s' % MIN_DELAY
1593 if delay > MAX_DELAY:
1594 msg = '"' + str(delay) + '": Delay must be <=%s' % MAX_DELAY
1599 """Returns processed program arguments."""
1600 description_text = """
1601 This script displays various statistics about VMs running under KVM.
1602 The statistics are gathered from the KVM debugfs entries and / or the
1603 currently available perf traces.
1605 The monitoring takes additional cpu cycles and might affect the VM's
1613 - /proc/sys/kernel/perf_event_paranoid < 1 if user has no
1614 CAP_SYS_ADMIN and perf events are used.
1615 - CAP_SYS_RESOURCE if the hard limit is not high enough to allow
1616 the large number of files that are possibly opened.
1618 Interactive Commands:
1619 b toggle events by guests (debugfs only, honors filters)
1621 f filter by regular expression
1622 g filter by guest name
1623 h display interactive commands reference
1624 o toggle sorting order (Total vs CurAvg/s)
1628 s set update interval (value range: 0.1-25.5 secs)
1629 x toggle reporting of stats for individual child trace events
1630 Press any other key to refresh statistics immediately.
1631 """ % (PATH_DEBUGFS_KVM, PATH_DEBUGFS_TRACING)
1633 class Guest_to_pid(argparse.Action):
1634 def __call__(self, parser, namespace, values, option_string=None):
1636 pids = Tui.get_pid_from_gname(values)
1638 sys.exit('Error while searching for guest "{}". Use "-p" to '
1639 'specify a pid instead?'.format(values))
1641 sys.exit('Error: No guest by the name "{}" found'
1644 sys.exit('Error: Multiple processes found (pids: {}). Use "-p"'
1645 ' to specify the desired pid'.format(" ".join(pids)))
1646 namespace.pid = pids[0]
1648 argparser = argparse.ArgumentParser(description=description_text,
1649 formatter_class=argparse
1650 .RawTextHelpFormatter)
1651 argparser.add_argument('-1', '--once', '--batch',
1652 action='store_true',
1654 help='run in batch mode for one second',
1656 argparser.add_argument('-c', '--csv',
1657 action='store_true',
1659 help='log in csv format - requires option -l/-L',
1661 argparser.add_argument('-d', '--debugfs',
1662 action='store_true',
1664 help='retrieve statistics from debugfs',
1666 argparser.add_argument('-f', '--fields',
1668 help='''fields to display (regex)
1669 "-f help" for a list of available events''',
1671 argparser.add_argument('-g', '--guest',
1673 help='restrict statistics to guest by name',
1674 action=Guest_to_pid,
1676 argparser.add_argument('-i', '--debugfs-include-past',
1677 action='store_true',
1679 help='include all available data on past events for'
1682 argparser.add_argument('-l', '--log',
1683 action='store_true',
1685 help='run in logging mode (like vmstat)',
1687 argparser.add_argument('-L', '--log-to-file',
1690 help="like '--log', but logging to a file"
1692 argparser.add_argument('-p', '--pid',
1695 help='restrict statistics to pid',
1697 argparser.add_argument('-s', '--set-delay',
1699 default=DELAY_DEFAULT,
1701 help='set delay between refreshs (value range: '
1702 '%s-%s secs)' % (MIN_DELAY, MAX_DELAY),
1704 argparser.add_argument('-t', '--tracepoints',
1705 action='store_true',
1707 help='retrieve statistics from tracepoints',
1709 argparser.add_argument('-z', '--skip-zero-records',
1710 action='store_true',
1712 help='omit records with all zeros in logging mode',
1714 options = argparser.parse_args()
1715 if options.csv and not (options.log or options.log_to_file):
1716 sys.exit('Error: Option -c/--csv requires -l/--log')
1717 if options.skip_zero_records and not (options.log or options.log_to_file):
1718 sys.exit('Error: Option -z/--skip-zero-records requires -l/-L')
1720 # verify that we were passed a valid regex up front
1721 re.compile(options.fields)
1723 sys.exit('Error: "' + options.fields + '" is not a valid regular '
1729 def check_access(options):
1730 """Exits if the current user can't access all needed directories."""
1731 if not os.path.exists(PATH_DEBUGFS_TRACING) and (options.tracepoints or
1732 not options.debugfs):
1733 sys.stderr.write("Please enable CONFIG_TRACING in your kernel "
1734 "when using the option -t (default).\n"
1735 "If it is enabled, make {0} readable by the "
1737 .format(PATH_DEBUGFS_TRACING))
1738 if options.tracepoints:
1741 sys.stderr.write("Falling back to debugfs statistics!\n")
1742 options.debugfs = True
1748 def assign_globals():
1749 global PATH_DEBUGFS_KVM
1750 global PATH_DEBUGFS_TRACING
1753 for line in open('/proc/mounts'):
1754 if line.split(' ')[0] == 'debugfs':
1755 debugfs = line.split(' ')[1]
1758 sys.stderr.write("Please make sure that CONFIG_DEBUG_FS is enabled in "
1759 "your kernel, mounted and\nreadable by the current "
1761 "('mount -t debugfs debugfs /sys/kernel/debug')\n")
1764 PATH_DEBUGFS_KVM = os.path.join(debugfs, 'kvm')
1765 PATH_DEBUGFS_TRACING = os.path.join(debugfs, 'tracing')
1767 if not os.path.exists(PATH_DEBUGFS_KVM):
1768 sys.stderr.write("Please make sure that CONFIG_KVM is enabled in "
1769 "your kernel and that the modules are loaded.\n")
1775 options = get_options()
1776 options = check_access(options)
1778 if (options.pid > 0 and
1779 not os.path.isdir(os.path.join('/proc/',
1780 str(options.pid)))):
1781 sys.stderr.write('Did you use a (unsupported) tid instead of a pid?\n')
1782 sys.exit('Specified pid does not exist.')
1784 err = is_delay_valid(options.set_delay)
1786 sys.exit('Error: ' + err)
1788 stats = Stats(options)
1790 if options.fields == 'help':
1791 stats.fields_filter = None
1793 for key in stats.get().keys():
1794 event_list.append(key.split('(', 1)[0])
1795 sys.stdout.write(' ' + '\n '.join(sorted(set(event_list))) + '\n')
1798 if options.log or options.log_to_file:
1799 if options.log_to_file:
1800 signal.signal(signal.SIGHUP, handle_signal)
1801 keys = sorted(stats.get().keys())
1803 frmt = CSVFormat(keys)
1805 frmt = StdFormat(keys)
1806 log(stats, options, frmt, keys)
1807 elif not options.once:
1808 with Tui(stats, options) as tui:
1814 if __name__ == "__main__":