.. | .. |
---|
1 | | -#!/usr/bin/python |
---|
| 1 | +#!/usr/bin/env python3 |
---|
| 2 | +# SPDX-License-Identifier: GPL-2.0-only |
---|
2 | 3 | # |
---|
3 | 4 | # top-like utility for displaying kvm statistics |
---|
4 | 5 | # |
---|
.. | .. |
---|
8 | 9 | # Authors: |
---|
9 | 10 | # Avi Kivity <avi@redhat.com> |
---|
10 | 11 | # |
---|
11 | | -# This work is licensed under the terms of the GNU GPL, version 2. See |
---|
12 | | -# the COPYING file in the top-level directory. |
---|
13 | 12 | """The kvm_stat module outputs statistics about running KVM VMs |
---|
14 | 13 | |
---|
15 | 14 | Three different ways of output formatting are available: |
---|
.. | .. |
---|
26 | 25 | import locale |
---|
27 | 26 | import os |
---|
28 | 27 | import time |
---|
29 | | -import optparse |
---|
| 28 | +import argparse |
---|
30 | 29 | import ctypes |
---|
31 | 30 | import fcntl |
---|
32 | 31 | import resource |
---|
33 | 32 | import struct |
---|
34 | 33 | import re |
---|
35 | 34 | import subprocess |
---|
| 35 | +import signal |
---|
36 | 36 | from collections import defaultdict, namedtuple |
---|
| 37 | +from functools import reduce |
---|
| 38 | +from datetime import datetime |
---|
37 | 39 | |
---|
38 | 40 | VMX_EXIT_REASONS = { |
---|
39 | 41 | 'EXCEPTION_NMI': 0, |
---|
.. | .. |
---|
226 | 228 | 'DISABLE': 0x00002401, |
---|
227 | 229 | 'RESET': 0x00002403, |
---|
228 | 230 | } |
---|
| 231 | + |
---|
| 232 | +signal_received = False |
---|
229 | 233 | |
---|
230 | 234 | ENCODING = locale.getpreferredencoding(False) |
---|
231 | 235 | TRACE_FILTER = re.compile(r'^[^\(]*$') |
---|
.. | .. |
---|
738 | 742 | The fields are all available KVM debugfs files |
---|
739 | 743 | |
---|
740 | 744 | """ |
---|
741 | | - return self.walkdir(PATH_DEBUGFS_KVM)[2] |
---|
| 745 | + exempt_list = ['halt_poll_fail_ns', 'halt_poll_success_ns'] |
---|
| 746 | + fields = [field for field in self.walkdir(PATH_DEBUGFS_KVM)[2] |
---|
| 747 | + if field not in exempt_list] |
---|
| 748 | + |
---|
| 749 | + return fields |
---|
742 | 750 | |
---|
743 | 751 | def update_fields(self, fields_filter): |
---|
744 | 752 | """Refresh fields, applying fields_filter""" |
---|
.. | .. |
---|
874 | 882 | |
---|
875 | 883 | if options.debugfs: |
---|
876 | 884 | providers.append(DebugfsProvider(options.pid, options.fields, |
---|
877 | | - options.dbgfs_include_past)) |
---|
| 885 | + options.debugfs_include_past)) |
---|
878 | 886 | if options.tracepoints or not providers: |
---|
879 | 887 | providers.append(TracepointProvider(options.pid, options.fields)) |
---|
880 | 888 | |
---|
.. | .. |
---|
975 | 983 | MAX_GUEST_NAME_LEN = 48 |
---|
976 | 984 | MAX_REGEX_LEN = 44 |
---|
977 | 985 | SORT_DEFAULT = 0 |
---|
| 986 | +MIN_DELAY = 0.1 |
---|
| 987 | +MAX_DELAY = 25.5 |
---|
978 | 988 | |
---|
979 | 989 | |
---|
980 | 990 | class Tui(object): |
---|
981 | 991 | """Instruments curses to draw a nice text ui.""" |
---|
982 | | - def __init__(self, stats): |
---|
| 992 | + def __init__(self, stats, opts): |
---|
983 | 993 | self.stats = stats |
---|
984 | 994 | self.screen = None |
---|
985 | 995 | self._delay_initial = 0.25 |
---|
986 | | - self._delay_regular = DELAY_DEFAULT |
---|
| 996 | + self._delay_regular = opts.set_delay |
---|
987 | 997 | self._sorting = SORT_DEFAULT |
---|
988 | 998 | self._display_guests = 0 |
---|
989 | 999 | |
---|
.. | .. |
---|
1184 | 1194 | |
---|
1185 | 1195 | if not self._is_running_guest(self.stats.pid_filter): |
---|
1186 | 1196 | if self._gname: |
---|
1187 | | - try: # ...to identify the guest by name in case it's back |
---|
| 1197 | + try: # ...to identify the guest by name in case it's back |
---|
1188 | 1198 | pids = self.get_pid_from_gname(self._gname) |
---|
1189 | 1199 | if len(pids) == 1: |
---|
1190 | 1200 | self._refresh_header(pids[0]) |
---|
.. | .. |
---|
1283 | 1293 | ' p filter by guest name/PID', |
---|
1284 | 1294 | ' q quit', |
---|
1285 | 1295 | ' r reset stats', |
---|
1286 | | - ' s set update interval', |
---|
| 1296 | + ' s set delay between refreshs (value range: ' |
---|
| 1297 | + '%s-%s secs)' % (MIN_DELAY, MAX_DELAY), |
---|
1287 | 1298 | ' x toggle reporting of stats for individual child trace' |
---|
1288 | 1299 | ' events', |
---|
1289 | 1300 | 'Any other key refreshes statistics immediately') |
---|
.. | .. |
---|
1337 | 1348 | msg = '' |
---|
1338 | 1349 | while True: |
---|
1339 | 1350 | self.screen.erase() |
---|
1340 | | - self.screen.addstr(0, 0, 'Set update interval (defaults to %.1fs).' % |
---|
1341 | | - DELAY_DEFAULT, curses.A_BOLD) |
---|
| 1351 | + self.screen.addstr(0, 0, 'Set update interval (defaults to %.1fs).' |
---|
| 1352 | + % DELAY_DEFAULT, curses.A_BOLD) |
---|
1342 | 1353 | self.screen.addstr(4, 0, msg) |
---|
1343 | 1354 | self.screen.addstr(2, 0, 'Change delay from %.1fs to ' % |
---|
1344 | 1355 | self._delay_regular) |
---|
.. | .. |
---|
1349 | 1360 | try: |
---|
1350 | 1361 | if len(val) > 0: |
---|
1351 | 1362 | delay = float(val) |
---|
1352 | | - if delay < 0.1: |
---|
1353 | | - msg = '"' + str(val) + '": Value must be >=0.1' |
---|
1354 | | - continue |
---|
1355 | | - if delay > 25.5: |
---|
1356 | | - msg = '"' + str(val) + '": Value must be <=25.5' |
---|
| 1363 | + err = is_delay_valid(delay) |
---|
| 1364 | + if err is not None: |
---|
| 1365 | + msg = err |
---|
1357 | 1366 | continue |
---|
1358 | 1367 | else: |
---|
1359 | 1368 | delay = DELAY_DEFAULT |
---|
.. | .. |
---|
1489 | 1498 | pass |
---|
1490 | 1499 | |
---|
1491 | 1500 | |
---|
1492 | | -def log(stats): |
---|
| 1501 | +class 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 | + |
---|
| 1517 | +class 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 | + |
---|
| 1531 | +def log(stats, opts, frmt, keys): |
---|
1493 | 1532 | """Prints statistics as reiterating key block, multiple value blocks.""" |
---|
1494 | | - keys = sorted(stats.get().keys()) |
---|
1495 | | - |
---|
1496 | | - def banner(): |
---|
1497 | | - for key in keys: |
---|
1498 | | - print(key.split(' ')[0], end=' ') |
---|
1499 | | - print() |
---|
1500 | | - |
---|
1501 | | - def statline(): |
---|
1502 | | - s = stats.get() |
---|
1503 | | - for key in keys: |
---|
1504 | | - print(' %9d' % s[key].delta, end=' ') |
---|
1505 | | - print() |
---|
| 1533 | + global signal_received |
---|
1506 | 1534 | line = 0 |
---|
1507 | 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 |
---|
1508 | 1558 | while True: |
---|
1509 | 1559 | try: |
---|
1510 | | - time.sleep(1) |
---|
1511 | | - if line % banner_repeat == 0: |
---|
1512 | | - banner() |
---|
1513 | | - statline() |
---|
1514 | | - line += 1 |
---|
| 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 |
---|
1515 | 1577 | except KeyboardInterrupt: |
---|
1516 | 1578 | break |
---|
| 1579 | + |
---|
| 1580 | + if opts.log_to_file: |
---|
| 1581 | + f.close() |
---|
| 1582 | + |
---|
| 1583 | + |
---|
| 1584 | +def handle_signal(sig, frame): |
---|
| 1585 | + global signal_received |
---|
| 1586 | + |
---|
| 1587 | + signal_received = True |
---|
| 1588 | + |
---|
| 1589 | + return |
---|
| 1590 | + |
---|
| 1591 | + |
---|
| 1592 | +def 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 |
---|
1517 | 1600 | |
---|
1518 | 1601 | |
---|
1519 | 1602 | def get_options(): |
---|
.. | .. |
---|
1546 | 1629 | p filter by PID |
---|
1547 | 1630 | q quit |
---|
1548 | 1631 | r reset stats |
---|
1549 | | - s set update interval |
---|
| 1632 | + s set update interval (value range: 0.1-25.5 secs) |
---|
1550 | 1633 | x toggle reporting of stats for individual child trace events |
---|
1551 | 1634 | Press any other key to refresh statistics immediately. |
---|
1552 | 1635 | """ % (PATH_DEBUGFS_KVM, PATH_DEBUGFS_TRACING) |
---|
1553 | 1636 | |
---|
1554 | | - class PlainHelpFormatter(optparse.IndentedHelpFormatter): |
---|
1555 | | - def format_description(self, description): |
---|
1556 | | - if description: |
---|
1557 | | - return description + "\n" |
---|
1558 | | - else: |
---|
1559 | | - return "" |
---|
| 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] |
---|
1560 | 1652 | |
---|
1561 | | - def cb_guest_to_pid(option, opt, val, parser): |
---|
1562 | | - try: |
---|
1563 | | - pids = Tui.get_pid_from_gname(val) |
---|
1564 | | - except: |
---|
1565 | | - sys.exit('Error while searching for guest "{}". Use "-p" to ' |
---|
1566 | | - 'specify a pid instead?'.format(val)) |
---|
1567 | | - if len(pids) == 0: |
---|
1568 | | - sys.exit('Error: No guest by the name "{}" found'.format(val)) |
---|
1569 | | - if len(pids) > 1: |
---|
1570 | | - sys.exit('Error: Multiple processes found (pids: {}). Use "-p" ' |
---|
1571 | | - 'to specify the desired pid'.format(" ".join(pids))) |
---|
1572 | | - parser.values.pid = pids[0] |
---|
1573 | | - |
---|
1574 | | - optparser = optparse.OptionParser(description=description_text, |
---|
1575 | | - formatter=PlainHelpFormatter()) |
---|
1576 | | - optparser.add_option('-1', '--once', '--batch', |
---|
1577 | | - action='store_true', |
---|
1578 | | - default=False, |
---|
1579 | | - dest='once', |
---|
1580 | | - help='run in batch mode for one second', |
---|
1581 | | - ) |
---|
1582 | | - optparser.add_option('-i', '--debugfs-include-past', |
---|
1583 | | - action='store_true', |
---|
1584 | | - default=False, |
---|
1585 | | - dest='dbgfs_include_past', |
---|
1586 | | - help='include all available data on past events for ' |
---|
1587 | | - 'debugfs', |
---|
1588 | | - ) |
---|
1589 | | - optparser.add_option('-l', '--log', |
---|
1590 | | - action='store_true', |
---|
1591 | | - default=False, |
---|
1592 | | - dest='log', |
---|
1593 | | - help='run in logging mode (like vmstat)', |
---|
1594 | | - ) |
---|
1595 | | - optparser.add_option('-t', '--tracepoints', |
---|
1596 | | - action='store_true', |
---|
1597 | | - default=False, |
---|
1598 | | - dest='tracepoints', |
---|
1599 | | - help='retrieve statistics from tracepoints', |
---|
1600 | | - ) |
---|
1601 | | - optparser.add_option('-d', '--debugfs', |
---|
1602 | | - action='store_true', |
---|
1603 | | - default=False, |
---|
1604 | | - dest='debugfs', |
---|
1605 | | - help='retrieve statistics from debugfs', |
---|
1606 | | - ) |
---|
1607 | | - optparser.add_option('-f', '--fields', |
---|
1608 | | - action='store', |
---|
1609 | | - default='', |
---|
1610 | | - dest='fields', |
---|
1611 | | - help='''fields to display (regex) |
---|
1612 | | - "-f help" for a list of available events''', |
---|
1613 | | - ) |
---|
1614 | | - optparser.add_option('-p', '--pid', |
---|
1615 | | - action='store', |
---|
1616 | | - default=0, |
---|
1617 | | - type='int', |
---|
1618 | | - dest='pid', |
---|
1619 | | - help='restrict statistics to pid', |
---|
1620 | | - ) |
---|
1621 | | - optparser.add_option('-g', '--guest', |
---|
1622 | | - action='callback', |
---|
1623 | | - type='string', |
---|
1624 | | - dest='pid', |
---|
1625 | | - metavar='GUEST', |
---|
1626 | | - help='restrict statistics to guest by name', |
---|
1627 | | - callback=cb_guest_to_pid, |
---|
1628 | | - ) |
---|
1629 | | - options, unkn = optparser.parse_args(sys.argv) |
---|
1630 | | - if len(unkn) != 1: |
---|
1631 | | - sys.exit('Error: Extra argument(s): ' + ' '.join(unkn[1:])) |
---|
| 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') |
---|
1632 | 1724 | try: |
---|
1633 | 1725 | # verify that we were passed a valid regex up front |
---|
1634 | 1726 | re.compile(options.fields) |
---|
.. | .. |
---|
1694 | 1786 | sys.stderr.write('Did you use a (unsupported) tid instead of a pid?\n') |
---|
1695 | 1787 | sys.exit('Specified pid does not exist.') |
---|
1696 | 1788 | |
---|
| 1789 | + err = is_delay_valid(options.set_delay) |
---|
| 1790 | + if err is not None: |
---|
| 1791 | + sys.exit('Error: ' + err) |
---|
| 1792 | + |
---|
1697 | 1793 | stats = Stats(options) |
---|
1698 | 1794 | |
---|
1699 | 1795 | if options.fields == 'help': |
---|
.. | .. |
---|
1704 | 1800 | sys.stdout.write(' ' + '\n '.join(sorted(set(event_list))) + '\n') |
---|
1705 | 1801 | sys.exit(0) |
---|
1706 | 1802 | |
---|
1707 | | - if options.log: |
---|
1708 | | - log(stats) |
---|
| 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) |
---|
1709 | 1812 | elif not options.once: |
---|
1710 | | - with Tui(stats) as tui: |
---|
| 1813 | + with Tui(stats, options) as tui: |
---|
1711 | 1814 | tui.show_stats() |
---|
1712 | 1815 | else: |
---|
1713 | 1816 | batch(stats) |
---|
1714 | 1817 | |
---|
| 1818 | + |
---|
1715 | 1819 | if __name__ == "__main__": |
---|
1716 | 1820 | main() |
---|