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