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