glances_cpu.py 17.1 KB
Newer Older
A
Alessio Sergi 已提交
1 2
# -*- coding: utf-8 -*-
#
3
# This file is part of Glances.
A
Alessio Sergi 已提交
4
#
5
# Copyright (C) 2021 Nicolargo <nicolas@nicolargo.com>
A
Alessio Sergi 已提交
6 7 8 9 10 11 12 13 14 15 16 17 18
#
# Glances is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Glances is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
A
PEP 257  
Alessio Sergi 已提交
19 20

"""CPU plugin."""
A
Alessio Sergi 已提交
21

22
from glances.logger import logger
23
from glances.timer import getTimeSinceLastUpdate
24 25
from glances.compat import iterkeys
from glances.cpu_percent import cpu_percent
26 27
from glances.globals import LINUX
from glances.plugins.glances_core import Plugin as CorePlugin
A
flake8  
Alessio Sergi 已提交
28 29 30
from glances.plugins.glances_plugin import GlancesPlugin

import psutil
31

N
nicolargo 已提交
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
# Fields description
# {'total': 19.7, 'user': 3.4, 'nice': 0.0, 'system': 2.6, 'idle': 93.0, 'iowait': 0.1, 'irq': 0.0, 'softirq': 0.8, 'steal': 0.0, 'guest': 0.0, 'guest_nice': 0.0, 'time_since_update': 2.1306779384613037, 'cpucore': 4, 'ctx_switches': 11636, 'interrupts': 4463, 'soft_interrupts': 3227, 'syscalls': 0}
fields_description = {
    'total': {'description': 'Sum of all CPU percentages (except idle).',
              'unit': 'percent'},
    'system': {'description': 'percent time spent in kernel space. System CPU time is the \
time spent running code in the Operating System kernel.',
               'unit': 'percent'},
    'user': {'description': 'CPU percent time spent in user space. \
User CPU time is the time spent on the processor running your program\'s code (or code in libraries).',
             'unit': 'percent'},
    'iowait': {'description': '*(Linux)*: percent time spent by the CPU waiting for I/O \
operations to complete.',
               'unit': 'percent'},
    'idle': {'description': 'percent of CPU used by any program. Every program or task \
that runs on a computer system occupies a certain amount of processing \
time on the CPU. If the CPU has completed all tasks it is idle.',
             'unit': 'percent'},
    'irq': {'description': '*(Linux and BSD)*: percent time spent servicing/handling \
hardware/software interrupts. Time servicing interrupts (hardware + \
software).',
            'unit': 'percent'},
    'nice': {'description': '*(Unix)*: percent time occupied by user level processes with \
a positive nice value. The time the CPU has spent running users\' \
processes that have been *niced*.',
             'unit': 'percent'},
    'steal': {'description': '*(Linux)*: percentage of time a virtual CPU waits for a real \
CPU while the hypervisor is servicing another virtual processor.',
              'unit': 'percent'},
    'ctx_sw': {'description': 'number of context switches (voluntary + involuntary) per \
second. A context switch is a procedure that a computer\'s CPU (central \
processing unit) follows to change from one task (or process) to \
another while ensuring that the tasks do not conflict.',
               'unit': 'percent'},
    'inter': {'description': 'number of interrupts per second.',
              'unit': 'percent'},
    'sw_int': {'description': 'number of software interrupts per second. Always set to \
0 on Windows and SunOS.',
               'unit': 'percent'},
}

N
Nicolargo 已提交
73 74 75 76
# SNMP OID
# percentage of user CPU time: .1.3.6.1.4.1.2021.11.9.0
# percentages of system CPU time: .1.3.6.1.4.1.2021.11.10.0
# percentages of idle CPU time: .1.3.6.1.4.1.2021.11.11.0
N
Nicolargo 已提交
77 78
snmp_oid = {'default': {'user': '1.3.6.1.4.1.2021.11.9.0',
                        'system': '1.3.6.1.4.1.2021.11.10.0',
N
Nicolargo 已提交
79
                        'idle': '1.3.6.1.4.1.2021.11.11.0'},
N
Nicolargo 已提交
80
            'windows': {'percent': '1.3.6.1.2.1.25.3.3.1.2'},
N
Nicolargo 已提交
81
            'esxi': {'percent': '1.3.6.1.2.1.25.3.3.1.2'},
N
Nicolargo 已提交
82 83 84
            'netapp': {'system': '1.3.6.1.4.1.789.1.2.1.3.0',
                       'idle': '1.3.6.1.4.1.789.1.2.1.5.0',
                       'nb_log_core': '1.3.6.1.4.1.789.1.2.1.6.0'}}
A
Alessio Sergi 已提交
85

86
# Define the history items list
87
# - 'name' define the stat identifier
N
Nicolargo 已提交
88
# - 'y_unit' define the Y label
89 90 91 92 93 94
items_history_list = [{'name': 'user',
                       'description': 'User CPU usage',
                       'y_unit': '%'},
                      {'name': 'system',
                       'description': 'System CPU usage',
                       'y_unit': '%'}]
95

A
PEP 257  
Alessio Sergi 已提交
96

N
Nicolargo 已提交
97
class Plugin(GlancesPlugin):
A
Alessio Sergi 已提交
98 99 100 101
    """Glances CPU plugin.

    'stats' is a dictionary that contains the system-wide CPU utilization as a
    percentage.
A
Alessio Sergi 已提交
102 103
    """

104
    def __init__(self, args=None, config=None):
A
PEP 257  
Alessio Sergi 已提交
105
        """Init the CPU plugin."""
106 107
        super(Plugin, self).__init__(args=args,
                                     config=config,
N
nicolargo 已提交
108 109
                                     items_history_list=items_history_list,
                                     fields_description=fields_description)
A
Alessio Sergi 已提交
110 111 112 113

        # We want to display the stat in the curse interface
        self.display_curse = True

114 115 116 117 118 119
        # Call CorePlugin in order to display the core number
        try:
            self.nb_log_core = CorePlugin(args=self.args).update()["log"]
        except Exception:
            self.nb_log_core = 1

120
    @GlancesPlugin._check_decorator
121
    @GlancesPlugin._log_result_decorator
A
Alessio Sergi 已提交
122
    def update(self):
A
PEP 257  
Alessio Sergi 已提交
123
        """Update CPU stats using the input method."""
124
        # Grab stats into self.stats
125
        if self.input_method == 'local':
126
            stats = self.update_local()
127
        elif self.input_method == 'snmp':
128 129 130 131 132 133
            stats = self.update_snmp()
        else:
            stats = self.get_init_value()

        # Update the stats
        self.stats = stats
A
Alessio Sergi 已提交
134 135 136

        return self.stats

137
    def update_local(self):
A
Alessio Sergi 已提交
138
        """Update CPU stats using psutil."""
139 140 141 142 143
        # Grab CPU stats using psutil's cpu_percent and cpu_times_percent
        # Get all possible values for CPU stats: user, system, idle,
        # nice (UNIX), iowait (Linux), irq (Linux, FreeBSD), steal (Linux 2.6.11+)
        # The following stats are returned by the API but not displayed in the UI:
        # softirq (Linux), guest (Linux 2.6.24+), guest_nice (Linux 3.2.0+)
144 145 146 147 148

        # Init new stats
        stats = self.get_init_value()

        stats['total'] = cpu_percent.get()
149 150
        # Grab: 'user', 'system', 'idle', 'nice', 'iowait',
        #       'irq', 'softirq', 'steal', 'guest', 'guest_nice'
151
        cpu_times_percent = psutil.cpu_times_percent(interval=0.0)
152 153
        for stat in cpu_times_percent._fields:
            stats[stat] = getattr(cpu_times_percent, stat)
154

A
Alessio Sergi 已提交
155
        # Additional CPU stats (number of events not as a %; psutil>=4.1.0)
156 157 158 159
        # - ctx_switches: number of context switches (voluntary + involuntary) since boot.
        # - interrupts: number of interrupts since boot.
        # - soft_interrupts: number of software interrupts since boot. Always set to 0 on Windows and SunOS.
        # - syscalls: number of system calls since boot. Always set to 0 on Linux.
A
Alessio Sergi 已提交
160
        cpu_stats = psutil.cpu_stats()
161

A
Alessio Sergi 已提交
162 163 164
        # By storing time data we enable Rx/s and Tx/s calculations in the
        # XML/RPC API, which would otherwise be overly difficult work
        # for users of the API
165 166 167 168
        stats['time_since_update'] = getTimeSinceLastUpdate('cpu')

        # Core number is needed to compute the CTX switch limit
        stats['cpucore'] = self.nb_log_core
A
Alessio Sergi 已提交
169 170 171

        # Previous CPU stats are stored in the cpu_stats_old variable
        if not hasattr(self, 'cpu_stats_old'):
172 173 174 175
            # Init the stats (needed to have the key name for export)
            for stat in cpu_stats._fields:
                # @TODO: better to set it to None but should refactor views and UI...
                stats[stat] = 0
176
        else:
177
            # Others calls...
A
Alessio Sergi 已提交
178 179
            for stat in cpu_stats._fields:
                if getattr(cpu_stats, stat) is not None:
180
                    stats[stat] = getattr(cpu_stats, stat) - getattr(self.cpu_stats_old, stat)
A
Alessio Sergi 已提交
181

182 183
        # Save stats to compute next step
        self.cpu_stats_old = cpu_stats
184

185 186
        return stats

187 188
    def update_snmp(self):
        """Update CPU stats using SNMP."""
189 190 191 192

        # Init new stats
        stats = self.get_init_value()

193 194 195 196 197 198 199 200 201 202 203 204
        # Update stats using SNMP
        if self.short_system_name in ('windows', 'esxi'):
            # Windows or VMWare ESXi
            # You can find the CPU utilization of windows system by querying the oid
            # Give also the number of core (number of element in the table)
            try:
                cpu_stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name],
                                                bulk=True)
            except KeyError:
                self.reset()

            # Iter through CPU and compute the idle CPU stats
205 206
            stats['nb_log_core'] = 0
            stats['idle'] = 0
207 208
            for c in cpu_stats:
                if c.startswith('percent'):
209 210 211 212 213 214
                    stats['idle'] += float(cpu_stats['percent.3'])
                    stats['nb_log_core'] += 1
            if stats['nb_log_core'] > 0:
                stats['idle'] = stats['idle'] / stats['nb_log_core']
            stats['idle'] = 100 - stats['idle']
            stats['total'] = 100 - stats['idle']
215 216 217 218

        else:
            # Default behavor
            try:
219
                stats = self.get_stats_snmp(
220 221
                    snmp_oid=snmp_oid[self.short_system_name])
            except KeyError:
222
                stats = self.get_stats_snmp(
223 224
                    snmp_oid=snmp_oid['default'])

225
            if stats['idle'] == '':
226 227 228 229
                self.reset()
                return self.stats

            # Convert SNMP stats to float
230 231 232 233 234
            for key in iterkeys(stats):
                stats[key] = float(stats[key])
            stats['total'] = 100 - stats['idle']

        return stats
235

236
    def update_views(self):
A
PEP 257  
Alessio Sergi 已提交
237
        """Update stats views."""
238
        # Call the father's method
A
Alessio Sergi 已提交
239
        super(Plugin, self).update_views()
240 241 242

        # Add specifics informations
        # Alert and log
243
        for key in ['user', 'system', 'iowait', 'total']:
244 245 246
            if key in self.stats:
                self.views[key]['decoration'] = self.get_alert_log(self.stats[key], header=key)
        # Alert only
247
        for key in ['steal']:
248 249
            if key in self.stats:
                self.views[key]['decoration'] = self.get_alert(self.stats[key], header=key)
250 251 252 253
        # Alert only but depend on Core number
        for key in ['ctx_switches']:
            if key in self.stats:
                self.views[key]['decoration'] = self.get_alert(self.stats[key], maximum=100 * self.stats['cpucore'], header=key)
254
        # Optional
255
        for key in ['nice', 'irq', 'idle', 'steal', 'ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls']:
256 257 258
            if key in self.stats:
                self.views[key]['optional'] = True

259
    def msg_curse(self, args=None, max_width=None):
A
PEP 257  
Alessio Sergi 已提交
260
        """Return the list to display in the UI."""
A
Alessio Sergi 已提交
261 262 263
        # Init the return message
        ret = []

264
        # Only process if stats exist and plugin not disable
N
nicolargo 已提交
265
        if not self.stats or self.args.percpu or self.is_disable():
N
Nicolas Hennion 已提交
266 267
            return ret

A
Alessio Sergi 已提交
268
        # Build the string message
N
Nicolargo 已提交
269 270
        # If user stat is not here, display only idle / total CPU usage (for
        # exemple on Windows OS)
271
        idle_tag = 'user' not in self.stats
272

A
Alessio Sergi 已提交
273
        # Header
274
        msg = '{}'.format('CPU')
A
Alessio Sergi 已提交
275
        ret.append(self.curse_add_line(msg, "TITLE"))
276 277 278 279 280 281 282 283
        trend_user = self.get_trend('user')
        trend_system = self.get_trend('system')
        if trend_user is None or trend_user is None:
            trend_cpu = None
        else:
            trend_cpu = trend_user + trend_system
        msg = ' {:4}'.format(self.trend_msg(trend_cpu))
        ret.append(self.curse_add_line(msg))
A
Alessio Sergi 已提交
284
        # Total CPU usage
285
        msg = '{:5.1f}%'.format(self.stats['total'])
286 287 288 289 290 291 292
        ret.append(self.curse_add_line(
            msg, self.get_views(key='total', option='decoration')))
        # Idle CPU
        if 'idle' in self.stats and not idle_tag:
            msg = '  {:8}'.format('idle:')
            ret.append(self.curse_add_line(msg))
            msg = '{:5.1f}%'.format(self.stats['idle'])
293
            ret.append(self.curse_add_line(msg))
294 295
        # ctx_switches
        if 'ctx_switches' in self.stats:
296
            msg = '  {:8}'.format('ctx_sw:')
297
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='ctx_switches', option='optional')))
298
            msg = '{:>5}'.format(self.auto_unit(int(self.stats['ctx_switches'] // self.stats['time_since_update']),
N
nicolargo 已提交
299
                                                min_symbol='K'))
300 301 302 303
            ret.append(self.curse_add_line(
                msg, self.get_views(key='ctx_switches', option='decoration'),
                optional=self.get_views(key='ctx_switches', option='optional')))

A
Alessio Sergi 已提交
304 305 306
        # New line
        ret.append(self.curse_new_line())
        # User CPU
307
        if 'user' in self.stats:
308
            msg = '{:8}'.format('user:')
A
Alessio Sergi 已提交
309
            ret.append(self.curse_add_line(msg))
310
            msg = '{:5.1f}%'.format(self.stats['user'])
N
Nicolargo 已提交
311
            ret.append(self.curse_add_line(
312
                msg, self.get_views(key='user', option='decoration')))
313
        elif 'idle' in self.stats:
314
            msg = '{:8}'.format('idle:')
315
            ret.append(self.curse_add_line(msg))
316
            msg = '{:5.1f}%'.format(self.stats['idle'])
317
            ret.append(self.curse_add_line(msg))
A
Alessio Sergi 已提交
318 319
        # IRQ CPU
        if 'irq' in self.stats:
320
            msg = '  {:8}'.format('irq:')
321
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='irq', option='optional')))
322
            msg = '{:5.1f}%'.format(self.stats['irq'])
323
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='irq', option='optional')))
324 325
        # interrupts
        if 'interrupts' in self.stats:
326
            msg = '  {:8}'.format('inter:')
327
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='interrupts', option='optional')))
328
            msg = '{:>5}'.format(int(self.stats['interrupts'] // self.stats['time_since_update']))
329 330
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='interrupts', option='optional')))

A
Alessio Sergi 已提交
331 332 333
        # New line
        ret.append(self.curse_new_line())
        # System CPU
334
        if 'system' in self.stats and not idle_tag:
335
            msg = '{:8}'.format('system:')
A
Alessio Sergi 已提交
336
            ret.append(self.curse_add_line(msg))
337
            msg = '{:5.1f}%'.format(self.stats['system'])
N
Nicolargo 已提交
338
            ret.append(self.curse_add_line(
339
                msg, self.get_views(key='system', option='decoration')))
340
        else:
341
            msg = '{:8}'.format('core:')
342
            ret.append(self.curse_add_line(msg))
343
            msg = '{:>6}'.format(self.stats['nb_log_core'])
344
            ret.append(self.curse_add_line(msg))
345 346 347
        # Nice CPU
        if 'nice' in self.stats:
            msg = '  {:8}'.format('nice:')
N
Nicolargo 已提交
348
            ret.append(self.curse_add_line(
349 350 351 352
                msg, optional=self.get_views(key='nice', option='optional')))
            msg = '{:5.1f}%'.format(self.stats['nice'])
            ret.append(self.curse_add_line(
                msg, optional=self.get_views(key='nice', option='optional')))
353 354
        # soft_interrupts
        if 'soft_interrupts' in self.stats:
355
            msg = '  {:8}'.format('sw_int:')
356
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='soft_interrupts', option='optional')))
357
            msg = '{:>5}'.format(int(self.stats['soft_interrupts'] // self.stats['time_since_update']))
358 359
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='soft_interrupts', option='optional')))

A
Alessio Sergi 已提交
360 361
        # New line
        ret.append(self.curse_new_line())
362 363 364 365 366 367 368 369 370
        # IOWait CPU
        if 'iowait' in self.stats:
            msg = '{:8}'.format('iowait:')
            ret.append(self.curse_add_line(
                msg, optional=self.get_views(key='iowait', option='optional')))
            msg = '{:5.1f}%'.format(self.stats['iowait'])
            ret.append(self.curse_add_line(
                msg, self.get_views(key='iowait', option='decoration'),
                optional=self.get_views(key='iowait', option='optional')))
A
Alessio Sergi 已提交
371 372
        # Steal CPU usage
        if 'steal' in self.stats:
373
            msg = '  {:8}'.format('steal:')
374
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='steal', option='optional')))
375
            msg = '{:5.1f}%'.format(self.stats['steal'])
N
Nicolargo 已提交
376
            ret.append(self.curse_add_line(
377 378
                msg, self.get_views(key='steal', option='decoration'),
                optional=self.get_views(key='steal', option='optional')))
379 380 381
        # syscalls
        # syscalls: number of system calls since boot. Always set to 0 on Linux. (do not display)
        if 'syscalls' in self.stats and not LINUX:
382
            msg = '  {:8}'.format('syscal:')
383
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='syscalls', option='optional')))
384
            msg = '{:>5}'.format(int(self.stats['syscalls'] // self.stats['time_since_update']))
385
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='syscalls', option='optional')))
A
Alessio Sergi 已提交
386 387 388

        # Return the message with decoration
        return ret