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