1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0-only 3# 4# top-like utility for displaying kvm statistics 5# 6# Copyright 2006-2008 Qumranet Technologies 7# Copyright 2008-2011 Red Hat, Inc. 8# 9# Authors: 10# Avi Kivity <avi@redhat.com> 11# 12"""The kvm_stat module outputs statistics about running KVM VMs 13 14Three 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 18 19The data is sampled from the KVM's debugfs entries and its perf events. 20""" 21from __future__ import print_function 22 23import curses 24import sys 25import locale 26import os 27import time 28import argparse 29import ctypes 30import fcntl 31import resource 32import struct 33import re 34import subprocess 35from collections import defaultdict, namedtuple 36from functools import reduce 37from datetime import datetime 38 39VMX_EXIT_REASONS = { 40 'EXCEPTION_NMI': 0, 41 'EXTERNAL_INTERRUPT': 1, 42 'TRIPLE_FAULT': 2, 43 'PENDING_INTERRUPT': 7, 44 'NMI_WINDOW': 8, 45 'TASK_SWITCH': 9, 46 'CPUID': 10, 47 'HLT': 12, 48 'INVLPG': 14, 49 'RDPMC': 15, 50 'RDTSC': 16, 51 'VMCALL': 18, 52 'VMCLEAR': 19, 53 'VMLAUNCH': 20, 54 'VMPTRLD': 21, 55 'VMPTRST': 22, 56 'VMREAD': 23, 57 'VMRESUME': 24, 58 'VMWRITE': 25, 59 'VMOFF': 26, 60 'VMON': 27, 61 'CR_ACCESS': 28, 62 'DR_ACCESS': 29, 63 'IO_INSTRUCTION': 30, 64 'MSR_READ': 31, 65 'MSR_WRITE': 32, 66 'INVALID_STATE': 33, 67 'MWAIT_INSTRUCTION': 36, 68 'MONITOR_INSTRUCTION': 39, 69 'PAUSE_INSTRUCTION': 40, 70 'MCE_DURING_VMENTRY': 41, 71 'TPR_BELOW_THRESHOLD': 43, 72 'APIC_ACCESS': 44, 73 'EPT_VIOLATION': 48, 74 'EPT_MISCONFIG': 49, 75 'WBINVD': 54, 76 'XSETBV': 55, 77 'APIC_WRITE': 56, 78 'INVPCID': 58, 79} 80 81SVM_EXIT_REASONS = { 82 'READ_CR0': 0x000, 83 'READ_CR3': 0x003, 84 'READ_CR4': 0x004, 85 'READ_CR8': 0x008, 86 'WRITE_CR0': 0x010, 87 'WRITE_CR3': 0x013, 88 'WRITE_CR4': 0x014, 89 'WRITE_CR8': 0x018, 90 'READ_DR0': 0x020, 91 'READ_DR1': 0x021, 92 'READ_DR2': 0x022, 93 'READ_DR3': 0x023, 94 'READ_DR4': 0x024, 95 'READ_DR5': 0x025, 96 'READ_DR6': 0x026, 97 'READ_DR7': 0x027, 98 'WRITE_DR0': 0x030, 99 'WRITE_DR1': 0x031, 100 'WRITE_DR2': 0x032, 101 'WRITE_DR3': 0x033, 102 'WRITE_DR4': 0x034, 103 'WRITE_DR5': 0x035, 104 'WRITE_DR6': 0x036, 105 'WRITE_DR7': 0x037, 106 'EXCP_BASE': 0x040, 107 'INTR': 0x060, 108 'NMI': 0x061, 109 'SMI': 0x062, 110 'INIT': 0x063, 111 'VINTR': 0x064, 112 'CR0_SEL_WRITE': 0x065, 113 'IDTR_READ': 0x066, 114 'GDTR_READ': 0x067, 115 'LDTR_READ': 0x068, 116 'TR_READ': 0x069, 117 'IDTR_WRITE': 0x06a, 118 'GDTR_WRITE': 0x06b, 119 'LDTR_WRITE': 0x06c, 120 'TR_WRITE': 0x06d, 121 'RDTSC': 0x06e, 122 'RDPMC': 0x06f, 123 'PUSHF': 0x070, 124 'POPF': 0x071, 125 'CPUID': 0x072, 126 'RSM': 0x073, 127 'IRET': 0x074, 128 'SWINT': 0x075, 129 'INVD': 0x076, 130 'PAUSE': 0x077, 131 'HLT': 0x078, 132 'INVLPG': 0x079, 133 'INVLPGA': 0x07a, 134 'IOIO': 0x07b, 135 'MSR': 0x07c, 136 'TASK_SWITCH': 0x07d, 137 'FERR_FREEZE': 0x07e, 138 'SHUTDOWN': 0x07f, 139 'VMRUN': 0x080, 140 'VMMCALL': 0x081, 141 'VMLOAD': 0x082, 142 'VMSAVE': 0x083, 143 'STGI': 0x084, 144 'CLGI': 0x085, 145 'SKINIT': 0x086, 146 'RDTSCP': 0x087, 147 'ICEBP': 0x088, 148 'WBINVD': 0x089, 149 'MONITOR': 0x08a, 150 'MWAIT': 0x08b, 151 'MWAIT_COND': 0x08c, 152 'XSETBV': 0x08d, 153 'NPF': 0x400, 154} 155 156# EC definition of HSR (from arch/arm64/include/asm/kvm_arm.h) 157AARCH64_EXIT_REASONS = { 158 'UNKNOWN': 0x00, 159 'WFI': 0x01, 160 'CP15_32': 0x03, 161 'CP15_64': 0x04, 162 'CP14_MR': 0x05, 163 'CP14_LS': 0x06, 164 'FP_ASIMD': 0x07, 165 'CP10_ID': 0x08, 166 'CP14_64': 0x0C, 167 'ILL_ISS': 0x0E, 168 'SVC32': 0x11, 169 'HVC32': 0x12, 170 'SMC32': 0x13, 171 'SVC64': 0x15, 172 'HVC64': 0x16, 173 'SMC64': 0x17, 174 'SYS64': 0x18, 175 'IABT': 0x20, 176 'IABT_HYP': 0x21, 177 'PC_ALIGN': 0x22, 178 'DABT': 0x24, 179 'DABT_HYP': 0x25, 180 'SP_ALIGN': 0x26, 181 'FP_EXC32': 0x28, 182 'FP_EXC64': 0x2C, 183 'SERROR': 0x2F, 184 'BREAKPT': 0x30, 185 'BREAKPT_HYP': 0x31, 186 'SOFTSTP': 0x32, 187 'SOFTSTP_HYP': 0x33, 188 'WATCHPT': 0x34, 189 'WATCHPT_HYP': 0x35, 190 'BKPT32': 0x38, 191 'VECTOR32': 0x3A, 192 'BRK64': 0x3C, 193} 194 195# From include/uapi/linux/kvm.h, KVM_EXIT_xxx 196USERSPACE_EXIT_REASONS = { 197 'UNKNOWN': 0, 198 'EXCEPTION': 1, 199 'IO': 2, 200 'HYPERCALL': 3, 201 'DEBUG': 4, 202 'HLT': 5, 203 'MMIO': 6, 204 'IRQ_WINDOW_OPEN': 7, 205 'SHUTDOWN': 8, 206 'FAIL_ENTRY': 9, 207 'INTR': 10, 208 'SET_TPR': 11, 209 'TPR_ACCESS': 12, 210 'S390_SIEIC': 13, 211 'S390_RESET': 14, 212 'DCR': 15, 213 'NMI': 16, 214 'INTERNAL_ERROR': 17, 215 'OSI': 18, 216 'PAPR_HCALL': 19, 217 'S390_UCONTROL': 20, 218 'WATCHDOG': 21, 219 'S390_TSCH': 22, 220 'EPR': 23, 221 'SYSTEM_EVENT': 24, 222} 223 224IOCTL_NUMBERS = { 225 'SET_FILTER': 0x40082406, 226 'ENABLE': 0x00002400, 227 'DISABLE': 0x00002401, 228 'RESET': 0x00002403, 229} 230 231ENCODING = locale.getpreferredencoding(False) 232TRACE_FILTER = re.compile(r'^[^\(]*$') 233 234 235class Arch(object): 236 """Encapsulates global architecture specific data. 237 238 Contains the performance event open syscall and ioctl numbers, as 239 well as the VM exit reasons for the architecture it runs on. 240 241 """ 242 @staticmethod 243 def get_arch(): 244 machine = os.uname()[4] 245 246 if machine.startswith('ppc'): 247 return ArchPPC() 248 elif machine.startswith('aarch64'): 249 return ArchA64() 250 elif machine.startswith('s390'): 251 return ArchS390() 252 else: 253 # X86_64 254 for line in open('/proc/cpuinfo'): 255 if not line.startswith('flags'): 256 continue 257 258 flags = line.split() 259 if 'vmx' in flags: 260 return ArchX86(VMX_EXIT_REASONS) 261 if 'svm' in flags: 262 return ArchX86(SVM_EXIT_REASONS) 263 return 264 265 def tracepoint_is_child(self, field): 266 if (TRACE_FILTER.match(field)): 267 return None 268 return field.split('(', 1)[0] 269 270 271class ArchX86(Arch): 272 def __init__(self, exit_reasons): 273 self.sc_perf_evt_open = 298 274 self.ioctl_numbers = IOCTL_NUMBERS 275 self.exit_reason_field = 'exit_reason' 276 self.exit_reasons = exit_reasons 277 278 def debugfs_is_child(self, field): 279 """ Returns name of parent if 'field' is a child, None otherwise """ 280 return None 281 282 283class ArchPPC(Arch): 284 def __init__(self): 285 self.sc_perf_evt_open = 319 286 self.ioctl_numbers = IOCTL_NUMBERS 287 self.ioctl_numbers['ENABLE'] = 0x20002400 288 self.ioctl_numbers['DISABLE'] = 0x20002401 289 self.ioctl_numbers['RESET'] = 0x20002403 290 291 # PPC comes in 32 and 64 bit and some generated ioctl 292 # numbers depend on the wordsize. 293 char_ptr_size = ctypes.sizeof(ctypes.c_char_p) 294 self.ioctl_numbers['SET_FILTER'] = 0x80002406 | char_ptr_size << 16 295 self.exit_reason_field = 'exit_nr' 296 self.exit_reasons = {} 297 298 def debugfs_is_child(self, field): 299 """ Returns name of parent if 'field' is a child, None otherwise """ 300 return None 301 302 303class ArchA64(Arch): 304 def __init__(self): 305 self.sc_perf_evt_open = 241 306 self.ioctl_numbers = IOCTL_NUMBERS 307 self.exit_reason_field = 'esr_ec' 308 self.exit_reasons = AARCH64_EXIT_REASONS 309 310 def debugfs_is_child(self, field): 311 """ Returns name of parent if 'field' is a child, None otherwise """ 312 return None 313 314 315class ArchS390(Arch): 316 def __init__(self): 317 self.sc_perf_evt_open = 331 318 self.ioctl_numbers = IOCTL_NUMBERS 319 self.exit_reason_field = None 320 self.exit_reasons = None 321 322 def debugfs_is_child(self, field): 323 """ Returns name of parent if 'field' is a child, None otherwise """ 324 if field.startswith('instruction_'): 325 return 'exit_instruction' 326 327 328ARCH = Arch.get_arch() 329 330 331class perf_event_attr(ctypes.Structure): 332 """Struct that holds the necessary data to set up a trace event. 333 334 For an extensive explanation see perf_event_open(2) and 335 include/uapi/linux/perf_event.h, struct perf_event_attr 336 337 All fields that are not initialized in the constructor are 0. 338 339 """ 340 _fields_ = [('type', ctypes.c_uint32), 341 ('size', ctypes.c_uint32), 342 ('config', ctypes.c_uint64), 343 ('sample_freq', ctypes.c_uint64), 344 ('sample_type', ctypes.c_uint64), 345 ('read_format', ctypes.c_uint64), 346 ('flags', ctypes.c_uint64), 347 ('wakeup_events', ctypes.c_uint32), 348 ('bp_type', ctypes.c_uint32), 349 ('bp_addr', ctypes.c_uint64), 350 ('bp_len', ctypes.c_uint64), 351 ] 352 353 def __init__(self): 354 super(self.__class__, self).__init__() 355 self.type = PERF_TYPE_TRACEPOINT 356 self.size = ctypes.sizeof(self) 357 self.read_format = PERF_FORMAT_GROUP 358 359 360PERF_TYPE_TRACEPOINT = 2 361PERF_FORMAT_GROUP = 1 << 3 362 363 364class Group(object): 365 """Represents a perf event group.""" 366 367 def __init__(self): 368 self.events = [] 369 370 def add_event(self, event): 371 self.events.append(event) 372 373 def read(self): 374 """Returns a dict with 'event name: value' for all events in the 375 group. 376 377 Values are read by reading from the file descriptor of the 378 event that is the group leader. See perf_event_open(2) for 379 details. 380 381 Read format for the used event configuration is: 382 struct read_format { 383 u64 nr; /* The number of events */ 384 struct { 385 u64 value; /* The value of the event */ 386 } values[nr]; 387 }; 388 389 """ 390 length = 8 * (1 + len(self.events)) 391 read_format = 'xxxxxxxx' + 'Q' * len(self.events) 392 return dict(zip([event.name for event in self.events], 393 struct.unpack(read_format, 394 os.read(self.events[0].fd, length)))) 395 396 397class Event(object): 398 """Represents a performance event and manages its life cycle.""" 399 def __init__(self, name, group, trace_cpu, trace_pid, trace_point, 400 trace_filter, trace_set='kvm'): 401 self.libc = ctypes.CDLL('libc.so.6', use_errno=True) 402 self.syscall = self.libc.syscall 403 self.name = name 404 self.fd = None 405 self._setup_event(group, trace_cpu, trace_pid, trace_point, 406 trace_filter, trace_set) 407 408 def __del__(self): 409 """Closes the event's file descriptor. 410 411 As no python file object was created for the file descriptor, 412 python will not reference count the descriptor and will not 413 close it itself automatically, so we do it. 414 415 """ 416 if self.fd: 417 os.close(self.fd) 418 419 def _perf_event_open(self, attr, pid, cpu, group_fd, flags): 420 """Wrapper for the sys_perf_evt_open() syscall. 421 422 Used to set up performance events, returns a file descriptor or -1 423 on error. 424 425 Attributes are: 426 - syscall number 427 - struct perf_event_attr * 428 - pid or -1 to monitor all pids 429 - cpu number or -1 to monitor all cpus 430 - The file descriptor of the group leader or -1 to create a group. 431 - flags 432 433 """ 434 return self.syscall(ARCH.sc_perf_evt_open, ctypes.pointer(attr), 435 ctypes.c_int(pid), ctypes.c_int(cpu), 436 ctypes.c_int(group_fd), ctypes.c_long(flags)) 437 438 def _setup_event_attribute(self, trace_set, trace_point): 439 """Returns an initialized ctype perf_event_attr struct.""" 440 441 id_path = os.path.join(PATH_DEBUGFS_TRACING, 'events', trace_set, 442 trace_point, 'id') 443 444 event_attr = perf_event_attr() 445 event_attr.config = int(open(id_path).read()) 446 return event_attr 447 448 def _setup_event(self, group, trace_cpu, trace_pid, trace_point, 449 trace_filter, trace_set): 450 """Sets up the perf event in Linux. 451 452 Issues the syscall to register the event in the kernel and 453 then sets the optional filter. 454 455 """ 456 457 event_attr = self._setup_event_attribute(trace_set, trace_point) 458 459 # First event will be group leader. 460 group_leader = -1 461 462 # All others have to pass the leader's descriptor instead. 463 if group.events: 464 group_leader = group.events[0].fd 465 466 fd = self._perf_event_open(event_attr, trace_pid, 467 trace_cpu, group_leader, 0) 468 if fd == -1: 469 err = ctypes.get_errno() 470 raise OSError(err, os.strerror(err), 471 'while calling sys_perf_event_open().') 472 473 if trace_filter: 474 fcntl.ioctl(fd, ARCH.ioctl_numbers['SET_FILTER'], 475 trace_filter) 476 477 self.fd = fd 478 479 def enable(self): 480 """Enables the trace event in the kernel. 481 482 Enabling the group leader makes reading counters from it and the 483 events under it possible. 484 485 """ 486 fcntl.ioctl(self.fd, ARCH.ioctl_numbers['ENABLE'], 0) 487 488 def disable(self): 489 """Disables the trace event in the kernel. 490 491 Disabling the group leader makes reading all counters under it 492 impossible. 493 494 """ 495 fcntl.ioctl(self.fd, ARCH.ioctl_numbers['DISABLE'], 0) 496 497 def reset(self): 498 """Resets the count of the trace event in the kernel.""" 499 fcntl.ioctl(self.fd, ARCH.ioctl_numbers['RESET'], 0) 500 501 502class Provider(object): 503 """Encapsulates functionalities used by all providers.""" 504 def __init__(self, pid): 505 self.child_events = False 506 self.pid = pid 507 508 @staticmethod 509 def is_field_wanted(fields_filter, field): 510 """Indicate whether field is valid according to fields_filter.""" 511 if not fields_filter: 512 return True 513 return re.match(fields_filter, field) is not None 514 515 @staticmethod 516 def walkdir(path): 517 """Returns os.walk() data for specified directory. 518 519 As it is only a wrapper it returns the same 3-tuple of (dirpath, 520 dirnames, filenames). 521 """ 522 return next(os.walk(path)) 523 524 525class TracepointProvider(Provider): 526 """Data provider for the stats class. 527 528 Manages the events/groups from which it acquires its data. 529 530 """ 531 def __init__(self, pid, fields_filter): 532 self.group_leaders = [] 533 self.filters = self._get_filters() 534 self.update_fields(fields_filter) 535 super(TracepointProvider, self).__init__(pid) 536 537 @staticmethod 538 def _get_filters(): 539 """Returns a dict of trace events, their filter ids and 540 the values that can be filtered. 541 542 Trace events can be filtered for special values by setting a 543 filter string via an ioctl. The string normally has the format 544 identifier==value. For each filter a new event will be created, to 545 be able to distinguish the events. 546 547 """ 548 filters = {} 549 filters['kvm_userspace_exit'] = ('reason', USERSPACE_EXIT_REASONS) 550 if ARCH.exit_reason_field and ARCH.exit_reasons: 551 filters['kvm_exit'] = (ARCH.exit_reason_field, ARCH.exit_reasons) 552 return filters 553 554 def _get_available_fields(self): 555 """Returns a list of available events of format 'event name(filter 556 name)'. 557 558 All available events have directories under 559 /sys/kernel/debug/tracing/events/ which export information 560 about the specific event. Therefore, listing the dirs gives us 561 a list of all available events. 562 563 Some events like the vm exit reasons can be filtered for 564 specific values. To take account for that, the routine below 565 creates special fields with the following format: 566 event name(filter name) 567 568 """ 569 path = os.path.join(PATH_DEBUGFS_TRACING, 'events', 'kvm') 570 fields = self.walkdir(path)[1] 571 extra = [] 572 for field in fields: 573 if field in self.filters: 574 filter_name_, filter_dicts = self.filters[field] 575 for name in filter_dicts: 576 extra.append(field + '(' + name + ')') 577 fields += extra 578 return fields 579 580 def update_fields(self, fields_filter): 581 """Refresh fields, applying fields_filter""" 582 self.fields = [field for field in self._get_available_fields() 583 if self.is_field_wanted(fields_filter, field)] 584 # add parents for child fields - otherwise we won't see any output! 585 for field in self._fields: 586 parent = ARCH.tracepoint_is_child(field) 587 if (parent and parent not in self._fields): 588 self.fields.append(parent) 589 590 @staticmethod 591 def _get_online_cpus(): 592 """Returns a list of cpu id integers.""" 593 def parse_int_list(list_string): 594 """Returns an int list from a string of comma separated integers and 595 integer ranges.""" 596 integers = [] 597 members = list_string.split(',') 598 599 for member in members: 600 if '-' not in member: 601 integers.append(int(member)) 602 else: 603 int_range = member.split('-') 604 integers.extend(range(int(int_range[0]), 605 int(int_range[1]) + 1)) 606 607 return integers 608 609 with open('/sys/devices/system/cpu/online') as cpu_list: 610 cpu_string = cpu_list.readline() 611 return parse_int_list(cpu_string) 612 613 def _setup_traces(self): 614 """Creates all event and group objects needed to be able to retrieve 615 data.""" 616 fields = self._get_available_fields() 617 if self._pid > 0: 618 # Fetch list of all threads of the monitored pid, as qemu 619 # starts a thread for each vcpu. 620 path = os.path.join('/proc', str(self._pid), 'task') 621 groupids = self.walkdir(path)[1] 622 else: 623 groupids = self._get_online_cpus() 624 625 # The constant is needed as a buffer for python libs, std 626 # streams and other files that the script opens. 627 newlim = len(groupids) * len(fields) + 50 628 try: 629 softlim_, hardlim = resource.getrlimit(resource.RLIMIT_NOFILE) 630 631 if hardlim < newlim: 632 # Now we need CAP_SYS_RESOURCE, to increase the hard limit. 633 resource.setrlimit(resource.RLIMIT_NOFILE, (newlim, newlim)) 634 else: 635 # Raising the soft limit is sufficient. 636 resource.setrlimit(resource.RLIMIT_NOFILE, (newlim, hardlim)) 637 638 except ValueError: 639 sys.exit("NOFILE rlimit could not be raised to {0}".format(newlim)) 640 641 for groupid in groupids: 642 group = Group() 643 for name in fields: 644 tracepoint = name 645 tracefilter = None 646 match = re.match(r'(.*)\((.*)\)', name) 647 if match: 648 tracepoint, sub = match.groups() 649 tracefilter = ('%s==%d\0' % 650 (self.filters[tracepoint][0], 651 self.filters[tracepoint][1][sub])) 652 653 # From perf_event_open(2): 654 # pid > 0 and cpu == -1 655 # This measures the specified process/thread on any CPU. 656 # 657 # pid == -1 and cpu >= 0 658 # This measures all processes/threads on the specified CPU. 659 trace_cpu = groupid if self._pid == 0 else -1 660 trace_pid = int(groupid) if self._pid != 0 else -1 661 662 group.add_event(Event(name=name, 663 group=group, 664 trace_cpu=trace_cpu, 665 trace_pid=trace_pid, 666 trace_point=tracepoint, 667 trace_filter=tracefilter)) 668 669 self.group_leaders.append(group) 670 671 @property 672 def fields(self): 673 return self._fields 674 675 @fields.setter 676 def fields(self, fields): 677 """Enables/disables the (un)wanted events""" 678 self._fields = fields 679 for group in self.group_leaders: 680 for index, event in enumerate(group.events): 681 if event.name in fields: 682 event.reset() 683 event.enable() 684 else: 685 # Do not disable the group leader. 686 # It would disable all of its events. 687 if index != 0: 688 event.disable() 689 690 @property 691 def pid(self): 692 return self._pid 693 694 @pid.setter 695 def pid(self, pid): 696 """Changes the monitored pid by setting new traces.""" 697 self._pid = pid 698 # The garbage collector will get rid of all Event/Group 699 # objects and open files after removing the references. 700 self.group_leaders = [] 701 self._setup_traces() 702 self.fields = self._fields 703 704 def read(self, by_guest=0): 705 """Returns 'event name: current value' for all enabled events.""" 706 ret = defaultdict(int) 707 for group in self.group_leaders: 708 for name, val in group.read().items(): 709 if name not in self._fields: 710 continue 711 parent = ARCH.tracepoint_is_child(name) 712 if parent: 713 name += ' ' + parent 714 ret[name] += val 715 return ret 716 717 def reset(self): 718 """Reset all field counters""" 719 for group in self.group_leaders: 720 for event in group.events: 721 event.reset() 722 723 724class DebugfsProvider(Provider): 725 """Provides data from the files that KVM creates in the kvm debugfs 726 folder.""" 727 def __init__(self, pid, fields_filter, include_past): 728 self.update_fields(fields_filter) 729 self._baseline = {} 730 self.do_read = True 731 self.paths = [] 732 super(DebugfsProvider, self).__init__(pid) 733 if include_past: 734 self._restore() 735 736 def _get_available_fields(self): 737 """"Returns a list of available fields. 738 739 The fields are all available KVM debugfs files 740 741 """ 742 return self.walkdir(PATH_DEBUGFS_KVM)[2] 743 744 def update_fields(self, fields_filter): 745 """Refresh fields, applying fields_filter""" 746 self._fields = [field for field in self._get_available_fields() 747 if self.is_field_wanted(fields_filter, field)] 748 # add parents for child fields - otherwise we won't see any output! 749 for field in self._fields: 750 parent = ARCH.debugfs_is_child(field) 751 if (parent and parent not in self._fields): 752 self.fields.append(parent) 753 754 @property 755 def fields(self): 756 return self._fields 757 758 @fields.setter 759 def fields(self, fields): 760 self._fields = fields 761 self.reset() 762 763 @property 764 def pid(self): 765 return self._pid 766 767 @pid.setter 768 def pid(self, pid): 769 self._pid = pid 770 if pid != 0: 771 vms = self.walkdir(PATH_DEBUGFS_KVM)[1] 772 if len(vms) == 0: 773 self.do_read = False 774 775 self.paths = list(filter(lambda x: "{}-".format(pid) in x, vms)) 776 777 else: 778 self.paths = [] 779 self.do_read = True 780 781 def _verify_paths(self): 782 """Remove invalid paths""" 783 for path in self.paths: 784 if not os.path.exists(os.path.join(PATH_DEBUGFS_KVM, path)): 785 self.paths.remove(path) 786 continue 787 788 def read(self, reset=0, by_guest=0): 789 """Returns a dict with format:'file name / field -> current value'. 790 791 Parameter 'reset': 792 0 plain read 793 1 reset field counts to 0 794 2 restore the original field counts 795 796 """ 797 results = {} 798 799 # If no debugfs filtering support is available, then don't read. 800 if not self.do_read: 801 return results 802 self._verify_paths() 803 804 paths = self.paths 805 if self._pid == 0: 806 paths = [] 807 for entry in os.walk(PATH_DEBUGFS_KVM): 808 for dir in entry[1]: 809 paths.append(dir) 810 for path in paths: 811 for field in self._fields: 812 value = self._read_field(field, path) 813 key = path + field 814 if reset == 1: 815 self._baseline[key] = value 816 if reset == 2: 817 self._baseline[key] = 0 818 if self._baseline.get(key, -1) == -1: 819 self._baseline[key] = value 820 parent = ARCH.debugfs_is_child(field) 821 if parent: 822 field = field + ' ' + parent 823 else: 824 if by_guest: 825 field = key.split('-')[0] # set 'field' to 'pid' 826 increment = value - self._baseline.get(key, 0) 827 if field in results: 828 results[field] += increment 829 else: 830 results[field] = increment 831 832 return results 833 834 def _read_field(self, field, path): 835 """Returns the value of a single field from a specific VM.""" 836 try: 837 return int(open(os.path.join(PATH_DEBUGFS_KVM, 838 path, 839 field)) 840 .read()) 841 except IOError: 842 return 0 843 844 def reset(self): 845 """Reset field counters""" 846 self._baseline = {} 847 self.read(1) 848 849 def _restore(self): 850 """Reset field counters""" 851 self._baseline = {} 852 self.read(2) 853 854 855EventStat = namedtuple('EventStat', ['value', 'delta']) 856 857 858class Stats(object): 859 """Manages the data providers and the data they provide. 860 861 It is used to set filters on the provider's data and collect all 862 provider data. 863 864 """ 865 def __init__(self, options): 866 self.providers = self._get_providers(options) 867 self._pid_filter = options.pid 868 self._fields_filter = options.fields 869 self.values = {} 870 self._child_events = False 871 872 def _get_providers(self, options): 873 """Returns a list of data providers depending on the passed options.""" 874 providers = [] 875 876 if options.debugfs: 877 providers.append(DebugfsProvider(options.pid, options.fields, 878 options.debugfs_include_past)) 879 if options.tracepoints or not providers: 880 providers.append(TracepointProvider(options.pid, options.fields)) 881 882 return providers 883 884 def _update_provider_filters(self): 885 """Propagates fields filters to providers.""" 886 # As we reset the counters when updating the fields we can 887 # also clear the cache of old values. 888 self.values = {} 889 for provider in self.providers: 890 provider.update_fields(self._fields_filter) 891 892 def reset(self): 893 self.values = {} 894 for provider in self.providers: 895 provider.reset() 896 897 @property 898 def fields_filter(self): 899 return self._fields_filter 900 901 @fields_filter.setter 902 def fields_filter(self, fields_filter): 903 if fields_filter != self._fields_filter: 904 self._fields_filter = fields_filter 905 self._update_provider_filters() 906 907 @property 908 def pid_filter(self): 909 return self._pid_filter 910 911 @pid_filter.setter 912 def pid_filter(self, pid): 913 if pid != self._pid_filter: 914 self._pid_filter = pid 915 self.values = {} 916 for provider in self.providers: 917 provider.pid = self._pid_filter 918 919 @property 920 def child_events(self): 921 return self._child_events 922 923 @child_events.setter 924 def child_events(self, val): 925 self._child_events = val 926 for provider in self.providers: 927 provider.child_events = val 928 929 def get(self, by_guest=0): 930 """Returns a dict with field -> (value, delta to last value) of all 931 provider data. 932 Key formats: 933 * plain: 'key' is event name 934 * child-parent: 'key' is in format '<child> <parent>' 935 * pid: 'key' is the pid of the guest, and the record contains the 936 aggregated event data 937 These formats are generated by the providers, and handled in class TUI. 938 """ 939 for provider in self.providers: 940 new = provider.read(by_guest=by_guest) 941 for key in new: 942 oldval = self.values.get(key, EventStat(0, 0)).value 943 newval = new.get(key, 0) 944 newdelta = newval - oldval 945 self.values[key] = EventStat(newval, newdelta) 946 return self.values 947 948 def toggle_display_guests(self, to_pid): 949 """Toggle between collection of stats by individual event and by 950 guest pid 951 952 Events reported by DebugfsProvider change when switching to/from 953 reading by guest values. Hence we have to remove the excess event 954 names from self.values. 955 956 """ 957 if any(isinstance(ins, TracepointProvider) for ins in self.providers): 958 return 1 959 if to_pid: 960 for provider in self.providers: 961 if isinstance(provider, DebugfsProvider): 962 for key in provider.fields: 963 if key in self.values.keys(): 964 del self.values[key] 965 else: 966 oldvals = self.values.copy() 967 for key in oldvals: 968 if key.isdigit(): 969 del self.values[key] 970 # Update oldval (see get()) 971 self.get(to_pid) 972 return 0 973 974 975DELAY_DEFAULT = 3.0 976MAX_GUEST_NAME_LEN = 48 977MAX_REGEX_LEN = 44 978SORT_DEFAULT = 0 979MIN_DELAY = 0.1 980MAX_DELAY = 25.5 981 982 983class Tui(object): 984 """Instruments curses to draw a nice text ui.""" 985 def __init__(self, stats, opts): 986 self.stats = stats 987 self.screen = None 988 self._delay_initial = 0.25 989 self._delay_regular = opts.set_delay 990 self._sorting = SORT_DEFAULT 991 self._display_guests = 0 992 993 def __enter__(self): 994 """Initialises curses for later use. Based on curses.wrapper 995 implementation from the Python standard library.""" 996 self.screen = curses.initscr() 997 curses.noecho() 998 curses.cbreak() 999 1000 # The try/catch works around a minor bit of 1001 # over-conscientiousness in the curses module, the error 1002 # return from C start_color() is ignorable. 1003 try: 1004 curses.start_color() 1005 except curses.error: 1006 pass 1007 1008 # Hide cursor in extra statement as some monochrome terminals 1009 # might support hiding but not colors. 1010 try: 1011 curses.curs_set(0) 1012 except curses.error: 1013 pass 1014 1015 curses.use_default_colors() 1016 return self 1017 1018 def __exit__(self, *exception): 1019 """Resets the terminal to its normal state. Based on curses.wrapper 1020 implementation from the Python standard library.""" 1021 if self.screen: 1022 self.screen.keypad(0) 1023 curses.echo() 1024 curses.nocbreak() 1025 curses.endwin() 1026 1027 @staticmethod 1028 def get_all_gnames(): 1029 """Returns a list of (pid, gname) tuples of all running guests""" 1030 res = [] 1031 try: 1032 child = subprocess.Popen(['ps', '-A', '--format', 'pid,args'], 1033 stdout=subprocess.PIPE) 1034 except: 1035 raise Exception 1036 for line in child.stdout: 1037 line = line.decode(ENCODING).lstrip().split(' ', 1) 1038 # perform a sanity check before calling the more expensive 1039 # function to possibly extract the guest name 1040 if ' -name ' in line[1]: 1041 res.append((line[0], Tui.get_gname_from_pid(line[0]))) 1042 child.stdout.close() 1043 1044 return res 1045 1046 def _print_all_gnames(self, row): 1047 """Print a list of all running guests along with their pids.""" 1048 self.screen.addstr(row, 2, '%8s %-60s' % 1049 ('Pid', 'Guest Name (fuzzy list, might be ' 1050 'inaccurate!)'), 1051 curses.A_UNDERLINE) 1052 row += 1 1053 try: 1054 for line in self.get_all_gnames(): 1055 self.screen.addstr(row, 2, '%8s %-60s' % (line[0], line[1])) 1056 row += 1 1057 if row >= self.screen.getmaxyx()[0]: 1058 break 1059 except Exception: 1060 self.screen.addstr(row + 1, 2, 'Not available') 1061 1062 @staticmethod 1063 def get_pid_from_gname(gname): 1064 """Fuzzy function to convert guest name to QEMU process pid. 1065 1066 Returns a list of potential pids, can be empty if no match found. 1067 Throws an exception on processing errors. 1068 1069 """ 1070 pids = [] 1071 for line in Tui.get_all_gnames(): 1072 if gname == line[1]: 1073 pids.append(int(line[0])) 1074 1075 return pids 1076 1077 @staticmethod 1078 def get_gname_from_pid(pid): 1079 """Returns the guest name for a QEMU process pid. 1080 1081 Extracts the guest name from the QEMU comma line by processing the 1082 '-name' option. Will also handle names specified out of sequence. 1083 1084 """ 1085 name = '' 1086 try: 1087 line = open('/proc/{}/cmdline' 1088 .format(pid), 'r').read().split('\0') 1089 parms = line[line.index('-name') + 1].split(',') 1090 while '' in parms: 1091 # commas are escaped (i.e. ',,'), hence e.g. 'foo,bar' results 1092 # in # ['foo', '', 'bar'], which we revert here 1093 idx = parms.index('') 1094 parms[idx - 1] += ',' + parms[idx + 1] 1095 del parms[idx:idx+2] 1096 # the '-name' switch allows for two ways to specify the guest name, 1097 # where the plain name overrides the name specified via 'guest=' 1098 for arg in parms: 1099 if '=' not in arg: 1100 name = arg 1101 break 1102 if arg[:6] == 'guest=': 1103 name = arg[6:] 1104 except (ValueError, IOError, IndexError): 1105 pass 1106 1107 return name 1108 1109 def _update_pid(self, pid): 1110 """Propagates pid selection to stats object.""" 1111 self.screen.addstr(4, 1, 'Updating pid filter...') 1112 self.screen.refresh() 1113 self.stats.pid_filter = pid 1114 1115 def _refresh_header(self, pid=None): 1116 """Refreshes the header.""" 1117 if pid is None: 1118 pid = self.stats.pid_filter 1119 self.screen.erase() 1120 gname = self.get_gname_from_pid(pid) 1121 self._gname = gname 1122 if gname: 1123 gname = ('({})'.format(gname[:MAX_GUEST_NAME_LEN] + '...' 1124 if len(gname) > MAX_GUEST_NAME_LEN 1125 else gname)) 1126 if pid > 0: 1127 self._headline = 'kvm statistics - pid {0} {1}'.format(pid, gname) 1128 else: 1129 self._headline = 'kvm statistics - summary' 1130 self.screen.addstr(0, 0, self._headline, curses.A_BOLD) 1131 if self.stats.fields_filter: 1132 regex = self.stats.fields_filter 1133 if len(regex) > MAX_REGEX_LEN: 1134 regex = regex[:MAX_REGEX_LEN] + '...' 1135 self.screen.addstr(1, 17, 'regex filter: {0}'.format(regex)) 1136 if self._display_guests: 1137 col_name = 'Guest Name' 1138 else: 1139 col_name = 'Event' 1140 self.screen.addstr(2, 1, '%-40s %10s%7s %8s' % 1141 (col_name, 'Total', '%Total', 'CurAvg/s'), 1142 curses.A_STANDOUT) 1143 self.screen.addstr(4, 1, 'Collecting data...') 1144 self.screen.refresh() 1145 1146 def _refresh_body(self, sleeptime): 1147 def insert_child(sorted_items, child, values, parent): 1148 num = len(sorted_items) 1149 for i in range(0, num): 1150 # only add child if parent is present 1151 if parent.startswith(sorted_items[i][0]): 1152 sorted_items.insert(i + 1, (' ' + child, values)) 1153 1154 def get_sorted_events(self, stats): 1155 """ separate parent and child events """ 1156 if self._sorting == SORT_DEFAULT: 1157 def sortkey(pair): 1158 # sort by (delta value, overall value) 1159 v = pair[1] 1160 return (v.delta, v.value) 1161 else: 1162 def sortkey(pair): 1163 # sort by overall value 1164 v = pair[1] 1165 return v.value 1166 1167 childs = [] 1168 sorted_items = [] 1169 # we can't rule out child events to appear prior to parents even 1170 # when sorted - separate out all children first, and add in later 1171 for key, values in sorted(stats.items(), key=sortkey, 1172 reverse=True): 1173 if values == (0, 0): 1174 continue 1175 if key.find(' ') != -1: 1176 if not self.stats.child_events: 1177 continue 1178 childs.insert(0, (key, values)) 1179 else: 1180 sorted_items.append((key, values)) 1181 if self.stats.child_events: 1182 for key, values in childs: 1183 (child, parent) = key.split(' ') 1184 insert_child(sorted_items, child, values, parent) 1185 1186 return sorted_items 1187 1188 if not self._is_running_guest(self.stats.pid_filter): 1189 if self._gname: 1190 try: # ...to identify the guest by name in case it's back 1191 pids = self.get_pid_from_gname(self._gname) 1192 if len(pids) == 1: 1193 self._refresh_header(pids[0]) 1194 self._update_pid(pids[0]) 1195 return 1196 except: 1197 pass 1198 self._display_guest_dead() 1199 # leave final data on screen 1200 return 1201 row = 3 1202 self.screen.move(row, 0) 1203 self.screen.clrtobot() 1204 stats = self.stats.get(self._display_guests) 1205 total = 0. 1206 ctotal = 0. 1207 for key, values in stats.items(): 1208 if self._display_guests: 1209 if self.get_gname_from_pid(key): 1210 total += values.value 1211 continue 1212 if not key.find(' ') != -1: 1213 total += values.value 1214 else: 1215 ctotal += values.value 1216 if total == 0.: 1217 # we don't have any fields, or all non-child events are filtered 1218 total = ctotal 1219 1220 # print events 1221 tavg = 0 1222 tcur = 0 1223 guest_removed = False 1224 for key, values in get_sorted_events(self, stats): 1225 if row >= self.screen.getmaxyx()[0] - 1 or values == (0, 0): 1226 break 1227 if self._display_guests: 1228 key = self.get_gname_from_pid(key) 1229 if not key: 1230 continue 1231 cur = int(round(values.delta / sleeptime)) if values.delta else 0 1232 if cur < 0: 1233 guest_removed = True 1234 continue 1235 if key[0] != ' ': 1236 if values.delta: 1237 tcur += values.delta 1238 ptotal = values.value 1239 ltotal = total 1240 else: 1241 ltotal = ptotal 1242 self.screen.addstr(row, 1, '%-40s %10d%7.1f %8s' % (key, 1243 values.value, 1244 values.value * 100 / float(ltotal), cur)) 1245 row += 1 1246 if row == 3: 1247 if guest_removed: 1248 self.screen.addstr(4, 1, 'Guest removed, updating...') 1249 else: 1250 self.screen.addstr(4, 1, 'No matching events reported yet') 1251 if row > 4: 1252 tavg = int(round(tcur / sleeptime)) if tcur > 0 else '' 1253 self.screen.addstr(row, 1, '%-40s %10d %8s' % 1254 ('Total', total, tavg), curses.A_BOLD) 1255 self.screen.refresh() 1256 1257 def _display_guest_dead(self): 1258 marker = ' Guest is DEAD ' 1259 y = min(len(self._headline), 80 - len(marker)) 1260 self.screen.addstr(0, y, marker, curses.A_BLINK | curses.A_STANDOUT) 1261 1262 def _show_msg(self, text): 1263 """Display message centered text and exit on key press""" 1264 hint = 'Press any key to continue' 1265 curses.cbreak() 1266 self.screen.erase() 1267 (x, term_width) = self.screen.getmaxyx() 1268 row = 2 1269 for line in text: 1270 start = (term_width - len(line)) // 2 1271 self.screen.addstr(row, start, line) 1272 row += 1 1273 self.screen.addstr(row + 1, (term_width - len(hint)) // 2, hint, 1274 curses.A_STANDOUT) 1275 self.screen.getkey() 1276 1277 def _show_help_interactive(self): 1278 """Display help with list of interactive commands""" 1279 msg = (' b toggle events by guests (debugfs only, honors' 1280 ' filters)', 1281 ' c clear filter', 1282 ' f filter by regular expression', 1283 ' g filter by guest name/PID', 1284 ' h display interactive commands reference', 1285 ' o toggle sorting order (Total vs CurAvg/s)', 1286 ' p filter by guest name/PID', 1287 ' q quit', 1288 ' r reset stats', 1289 ' s set delay between refreshs (value range: ' 1290 '%s-%s secs)' % (MIN_DELAY, MAX_DELAY), 1291 ' x toggle reporting of stats for individual child trace' 1292 ' events', 1293 'Any other key refreshes statistics immediately') 1294 curses.cbreak() 1295 self.screen.erase() 1296 self.screen.addstr(0, 0, "Interactive commands reference", 1297 curses.A_BOLD) 1298 self.screen.addstr(2, 0, "Press any key to exit", curses.A_STANDOUT) 1299 row = 4 1300 for line in msg: 1301 self.screen.addstr(row, 0, line) 1302 row += 1 1303 self.screen.getkey() 1304 self._refresh_header() 1305 1306 def _show_filter_selection(self): 1307 """Draws filter selection mask. 1308 1309 Asks for a valid regex and sets the fields filter accordingly. 1310 1311 """ 1312 msg = '' 1313 while True: 1314 self.screen.erase() 1315 self.screen.addstr(0, 0, 1316 "Show statistics for events matching a regex.", 1317 curses.A_BOLD) 1318 self.screen.addstr(2, 0, 1319 "Current regex: {0}" 1320 .format(self.stats.fields_filter)) 1321 self.screen.addstr(5, 0, msg) 1322 self.screen.addstr(3, 0, "New regex: ") 1323 curses.echo() 1324 regex = self.screen.getstr().decode(ENCODING) 1325 curses.noecho() 1326 if len(regex) == 0: 1327 self.stats.fields_filter = '' 1328 self._refresh_header() 1329 return 1330 try: 1331 re.compile(regex) 1332 self.stats.fields_filter = regex 1333 self._refresh_header() 1334 return 1335 except re.error: 1336 msg = '"' + regex + '": Not a valid regular expression' 1337 continue 1338 1339 def _show_set_update_interval(self): 1340 """Draws update interval selection mask.""" 1341 msg = '' 1342 while True: 1343 self.screen.erase() 1344 self.screen.addstr(0, 0, 'Set update interval (defaults to %.1fs).' 1345 % DELAY_DEFAULT, curses.A_BOLD) 1346 self.screen.addstr(4, 0, msg) 1347 self.screen.addstr(2, 0, 'Change delay from %.1fs to ' % 1348 self._delay_regular) 1349 curses.echo() 1350 val = self.screen.getstr().decode(ENCODING) 1351 curses.noecho() 1352 1353 try: 1354 if len(val) > 0: 1355 delay = float(val) 1356 err = is_delay_valid(delay) 1357 if err is not None: 1358 msg = err 1359 continue 1360 else: 1361 delay = DELAY_DEFAULT 1362 self._delay_regular = delay 1363 break 1364 1365 except ValueError: 1366 msg = '"' + str(val) + '": Invalid value' 1367 self._refresh_header() 1368 1369 def _is_running_guest(self, pid): 1370 """Check if pid is still a running process.""" 1371 if not pid: 1372 return True 1373 return os.path.isdir(os.path.join('/proc/', str(pid))) 1374 1375 def _show_vm_selection_by_guest(self): 1376 """Draws guest selection mask. 1377 1378 Asks for a guest name or pid until a valid guest name or '' is entered. 1379 1380 """ 1381 msg = '' 1382 while True: 1383 self.screen.erase() 1384 self.screen.addstr(0, 0, 1385 'Show statistics for specific guest or pid.', 1386 curses.A_BOLD) 1387 self.screen.addstr(1, 0, 1388 'This might limit the shown data to the trace ' 1389 'statistics.') 1390 self.screen.addstr(5, 0, msg) 1391 self._print_all_gnames(7) 1392 curses.echo() 1393 curses.curs_set(1) 1394 self.screen.addstr(3, 0, "Guest or pid [ENTER exits]: ") 1395 guest = self.screen.getstr().decode(ENCODING) 1396 curses.noecho() 1397 1398 pid = 0 1399 if not guest or guest == '0': 1400 break 1401 if guest.isdigit(): 1402 if not self._is_running_guest(guest): 1403 msg = '"' + guest + '": Not a running process' 1404 continue 1405 pid = int(guest) 1406 break 1407 pids = [] 1408 try: 1409 pids = self.get_pid_from_gname(guest) 1410 except: 1411 msg = '"' + guest + '": Internal error while searching, ' \ 1412 'use pid filter instead' 1413 continue 1414 if len(pids) == 0: 1415 msg = '"' + guest + '": Not an active guest' 1416 continue 1417 if len(pids) > 1: 1418 msg = '"' + guest + '": Multiple matches found, use pid ' \ 1419 'filter instead' 1420 continue 1421 pid = pids[0] 1422 break 1423 curses.curs_set(0) 1424 self._refresh_header(pid) 1425 self._update_pid(pid) 1426 1427 def show_stats(self): 1428 """Refreshes the screen and processes user input.""" 1429 sleeptime = self._delay_initial 1430 self._refresh_header() 1431 start = 0.0 # result based on init value never appears on screen 1432 while True: 1433 self._refresh_body(time.time() - start) 1434 curses.halfdelay(int(sleeptime * 10)) 1435 start = time.time() 1436 sleeptime = self._delay_regular 1437 try: 1438 char = self.screen.getkey() 1439 if char == 'b': 1440 self._display_guests = not self._display_guests 1441 if self.stats.toggle_display_guests(self._display_guests): 1442 self._show_msg(['Command not available with ' 1443 'tracepoints enabled', 'Restart with ' 1444 'debugfs only (see option \'-d\') and ' 1445 'try again!']) 1446 self._display_guests = not self._display_guests 1447 self._refresh_header() 1448 if char == 'c': 1449 self.stats.fields_filter = '' 1450 self._refresh_header(0) 1451 self._update_pid(0) 1452 if char == 'f': 1453 curses.curs_set(1) 1454 self._show_filter_selection() 1455 curses.curs_set(0) 1456 sleeptime = self._delay_initial 1457 if char == 'g' or char == 'p': 1458 self._show_vm_selection_by_guest() 1459 sleeptime = self._delay_initial 1460 if char == 'h': 1461 self._show_help_interactive() 1462 if char == 'o': 1463 self._sorting = not self._sorting 1464 if char == 'q': 1465 break 1466 if char == 'r': 1467 self.stats.reset() 1468 if char == 's': 1469 curses.curs_set(1) 1470 self._show_set_update_interval() 1471 curses.curs_set(0) 1472 sleeptime = self._delay_initial 1473 if char == 'x': 1474 self.stats.child_events = not self.stats.child_events 1475 except KeyboardInterrupt: 1476 break 1477 except curses.error: 1478 continue 1479 1480 1481def batch(stats): 1482 """Prints statistics in a key, value format.""" 1483 try: 1484 s = stats.get() 1485 time.sleep(1) 1486 s = stats.get() 1487 for key, values in sorted(s.items()): 1488 print('%-42s%10d%10d' % (key.split(' ')[0], values.value, 1489 values.delta)) 1490 except KeyboardInterrupt: 1491 pass 1492 1493 1494class StdFormat(object): 1495 def __init__(self, keys): 1496 self._banner = '' 1497 for key in keys: 1498 self._banner += key.split(' ')[0] + ' ' 1499 1500 def get_banner(self): 1501 return self._banner 1502 1503 @staticmethod 1504 def get_statline(keys, s): 1505 res = '' 1506 for key in keys: 1507 res += ' %9d' % s[key].delta 1508 return res 1509 1510 1511class CSVFormat(object): 1512 def __init__(self, keys): 1513 self._banner = 'timestamp' 1514 self._banner += reduce(lambda res, key: "{},{!s}".format(res, 1515 key.split(' ')[0]), keys, '') 1516 1517 def get_banner(self): 1518 return self._banner 1519 1520 @staticmethod 1521 def get_statline(keys, s): 1522 return reduce(lambda res, key: "{},{!s}".format(res, s[key].delta), 1523 keys, '') 1524 1525 1526def log(stats, opts, frmt, keys): 1527 """Prints statistics as reiterating key block, multiple value blocks.""" 1528 line = 0 1529 banner_repeat = 20 1530 while True: 1531 try: 1532 time.sleep(opts.set_delay) 1533 if line % banner_repeat == 0: 1534 print(frmt.get_banner()) 1535 print(datetime.now().strftime("%Y-%m-%d %H:%M:%S") + 1536 frmt.get_statline(keys, stats.get())) 1537 line += 1 1538 except KeyboardInterrupt: 1539 break 1540 1541 1542def is_delay_valid(delay): 1543 """Verify delay is in valid value range.""" 1544 msg = None 1545 if delay < MIN_DELAY: 1546 msg = '"' + str(delay) + '": Delay must be >=%s' % MIN_DELAY 1547 if delay > MAX_DELAY: 1548 msg = '"' + str(delay) + '": Delay must be <=%s' % MAX_DELAY 1549 return msg 1550 1551 1552def get_options(): 1553 """Returns processed program arguments.""" 1554 description_text = """ 1555This script displays various statistics about VMs running under KVM. 1556The statistics are gathered from the KVM debugfs entries and / or the 1557currently available perf traces. 1558 1559The monitoring takes additional cpu cycles and might affect the VM's 1560performance. 1561 1562Requirements: 1563- Access to: 1564 %s 1565 %s/events/* 1566 /proc/pid/task 1567- /proc/sys/kernel/perf_event_paranoid < 1 if user has no 1568 CAP_SYS_ADMIN and perf events are used. 1569- CAP_SYS_RESOURCE if the hard limit is not high enough to allow 1570 the large number of files that are possibly opened. 1571 1572Interactive Commands: 1573 b toggle events by guests (debugfs only, honors filters) 1574 c clear filter 1575 f filter by regular expression 1576 g filter by guest name 1577 h display interactive commands reference 1578 o toggle sorting order (Total vs CurAvg/s) 1579 p filter by PID 1580 q quit 1581 r reset stats 1582 s set update interval (value range: 0.1-25.5 secs) 1583 x toggle reporting of stats for individual child trace events 1584Press any other key to refresh statistics immediately. 1585""" % (PATH_DEBUGFS_KVM, PATH_DEBUGFS_TRACING) 1586 1587 class Guest_to_pid(argparse.Action): 1588 def __call__(self, parser, namespace, values, option_string=None): 1589 try: 1590 pids = Tui.get_pid_from_gname(values) 1591 except: 1592 sys.exit('Error while searching for guest "{}". Use "-p" to ' 1593 'specify a pid instead?'.format(values)) 1594 if len(pids) == 0: 1595 sys.exit('Error: No guest by the name "{}" found' 1596 .format(values)) 1597 if len(pids) > 1: 1598 sys.exit('Error: Multiple processes found (pids: {}). Use "-p"' 1599 ' to specify the desired pid'.format(" ".join(pids))) 1600 namespace.pid = pids[0] 1601 1602 argparser = argparse.ArgumentParser(description=description_text, 1603 formatter_class=argparse 1604 .RawTextHelpFormatter) 1605 argparser.add_argument('-1', '--once', '--batch', 1606 action='store_true', 1607 default=False, 1608 help='run in batch mode for one second', 1609 ) 1610 argparser.add_argument('-c', '--csv', 1611 action='store_true', 1612 default=False, 1613 help='log in csv format - requires option -l/--log', 1614 ) 1615 argparser.add_argument('-d', '--debugfs', 1616 action='store_true', 1617 default=False, 1618 help='retrieve statistics from debugfs', 1619 ) 1620 argparser.add_argument('-f', '--fields', 1621 default='', 1622 help='''fields to display (regex) 1623"-f help" for a list of available events''', 1624 ) 1625 argparser.add_argument('-g', '--guest', 1626 type=str, 1627 help='restrict statistics to guest by name', 1628 action=Guest_to_pid, 1629 ) 1630 argparser.add_argument('-i', '--debugfs-include-past', 1631 action='store_true', 1632 default=False, 1633 help='include all available data on past events for' 1634 ' debugfs', 1635 ) 1636 argparser.add_argument('-l', '--log', 1637 action='store_true', 1638 default=False, 1639 help='run in logging mode (like vmstat)', 1640 ) 1641 argparser.add_argument('-p', '--pid', 1642 type=int, 1643 default=0, 1644 help='restrict statistics to pid', 1645 ) 1646 argparser.add_argument('-s', '--set-delay', 1647 type=float, 1648 default=DELAY_DEFAULT, 1649 metavar='DELAY', 1650 help='set delay between refreshs (value range: ' 1651 '%s-%s secs)' % (MIN_DELAY, MAX_DELAY), 1652 ) 1653 argparser.add_argument('-t', '--tracepoints', 1654 action='store_true', 1655 default=False, 1656 help='retrieve statistics from tracepoints', 1657 ) 1658 options = argparser.parse_args() 1659 if options.csv and not options.log: 1660 sys.exit('Error: Option -c/--csv requires -l/--log') 1661 try: 1662 # verify that we were passed a valid regex up front 1663 re.compile(options.fields) 1664 except re.error: 1665 sys.exit('Error: "' + options.fields + '" is not a valid regular ' 1666 'expression') 1667 1668 return options 1669 1670 1671def check_access(options): 1672 """Exits if the current user can't access all needed directories.""" 1673 if not os.path.exists(PATH_DEBUGFS_TRACING) and (options.tracepoints or 1674 not options.debugfs): 1675 sys.stderr.write("Please enable CONFIG_TRACING in your kernel " 1676 "when using the option -t (default).\n" 1677 "If it is enabled, make {0} readable by the " 1678 "current user.\n" 1679 .format(PATH_DEBUGFS_TRACING)) 1680 if options.tracepoints: 1681 sys.exit(1) 1682 1683 sys.stderr.write("Falling back to debugfs statistics!\n") 1684 options.debugfs = True 1685 time.sleep(5) 1686 1687 return options 1688 1689 1690def assign_globals(): 1691 global PATH_DEBUGFS_KVM 1692 global PATH_DEBUGFS_TRACING 1693 1694 debugfs = '' 1695 for line in open('/proc/mounts'): 1696 if line.split(' ')[0] == 'debugfs': 1697 debugfs = line.split(' ')[1] 1698 break 1699 if debugfs == '': 1700 sys.stderr.write("Please make sure that CONFIG_DEBUG_FS is enabled in " 1701 "your kernel, mounted and\nreadable by the current " 1702 "user:\n" 1703 "('mount -t debugfs debugfs /sys/kernel/debug')\n") 1704 sys.exit(1) 1705 1706 PATH_DEBUGFS_KVM = os.path.join(debugfs, 'kvm') 1707 PATH_DEBUGFS_TRACING = os.path.join(debugfs, 'tracing') 1708 1709 if not os.path.exists(PATH_DEBUGFS_KVM): 1710 sys.stderr.write("Please make sure that CONFIG_KVM is enabled in " 1711 "your kernel and that the modules are loaded.\n") 1712 sys.exit(1) 1713 1714 1715def main(): 1716 assign_globals() 1717 options = get_options() 1718 options = check_access(options) 1719 1720 if (options.pid > 0 and 1721 not os.path.isdir(os.path.join('/proc/', 1722 str(options.pid)))): 1723 sys.stderr.write('Did you use a (unsupported) tid instead of a pid?\n') 1724 sys.exit('Specified pid does not exist.') 1725 1726 err = is_delay_valid(options.set_delay) 1727 if err is not None: 1728 sys.exit('Error: ' + err) 1729 1730 stats = Stats(options) 1731 1732 if options.fields == 'help': 1733 stats.fields_filter = None 1734 event_list = [] 1735 for key in stats.get().keys(): 1736 event_list.append(key.split('(', 1)[0]) 1737 sys.stdout.write(' ' + '\n '.join(sorted(set(event_list))) + '\n') 1738 sys.exit(0) 1739 1740 if options.log: 1741 keys = sorted(stats.get().keys()) 1742 if options.csv: 1743 frmt = CSVFormat(keys) 1744 else: 1745 frmt = StdFormat(keys) 1746 log(stats, options, frmt, keys) 1747 elif not options.once: 1748 with Tui(stats, options) as tui: 1749 tui.show_stats() 1750 else: 1751 batch(stats) 1752 1753 1754if __name__ == "__main__": 1755 main() 1756