Test Failed
Push — develop ( 934f60...056a10 )
by Nicolas
03:09 queued 32s
created

GlancesProcesses.__get_min_max_mean()   B

Complexity

Conditions 6

Size

Total Lines 26
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 21
nop 3
dl 0
loc 26
rs 8.4426
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# SPDX-FileCopyrightText: 2024 Nicolas Hennion <[email protected]>
6
#
7
# SPDX-License-Identifier: LGPL-3.0-only
8
#
9
10
import os
11
12
from glances.globals import BSD, LINUX, MACOS, WINDOWS, iterkeys
13
from glances.globals import namedtuple_to_dict, list_of_namedtuple_to_list_of_dict
14
from glances.timer import Timer, getTimeSinceLastUpdate
15
from glances.filter import GlancesFilterList, GlancesFilter
16
from glances.programs import processes_to_programs
17
from glances.logger import logger
18
19
import psutil
20
psutil_version_info = tuple([int(num) for num in psutil.__version__.split('.')])
21
22
# This constant defines the list of available processes sort key
23
sort_processes_key_list = ['cpu_percent', 'memory_percent', 'username', 'cpu_times', 'io_counters', 'name']
24
25
# Sort dictionary for human
26
sort_for_human = {
27
    'io_counters': 'disk IO',
28
    'cpu_percent': 'CPU consumption',
29
    'memory_percent': 'memory consumption',
30
    'cpu_times': 'process time',
31
    'username': 'user name',
32
    'name': 'processs name',
33
    None: 'None',
34
}
35
36
37
class GlancesProcesses(object):
38
    """Get processed stats using the psutil library."""
39
40
    def __init__(self, cache_timeout=60):
41
        """Init the class to collect stats about processes."""
42
        # Init the args, coming from the GlancesStandalone class
43
        # Should be set by the set_args method
44
        self.args = None
45
46
        # The internals caches will be cleaned each 'cache_timeout' seconds
47
        self.cache_timeout = cache_timeout
48
        # First iteration, no cache
49
        self.cache_timer = Timer(0)
50
51
        # Init the io_old dict used to compute the IO bitrate
52
        # key = pid
53
        # value = [ read_bytes_old, write_bytes_old ]
54
        self.io_old = {}
55
56
        # Init stats
57
        self.auto_sort = None
58
        self._sort_key = None
59
        # Default processes sort key is 'auto'
60
        # Can be overwrite from the configuration file (issue#1536) => See glances_processlist.py init
61
        self.set_sort_key('auto', auto=True)
62
        self.processlist = []
63
        self.reset_processcount()
64
65
        # Cache is a dict with key=pid and value = dict of cached value
66
        self.processlist_cache = {}
67
68
        # List of processes stats to export
69
        # Only process matching one of the filter will be exported
70
        self._filter_export = GlancesFilterList()
71
        self.processlist_export = []
72
73
        # Tag to enable/disable the processes stats (to reduce the Glances CPU consumption)
74
        # Default is to enable the processes stats
75
        self.disable_tag = False
76
77
        # Extended stats for top process is enable by default
78
        self.disable_extended_tag = False
79
        self.extended_process = None
80
81
        # Test if the system can grab io_counters
82
        try:
83
            p = psutil.Process()
84
            p.io_counters()
85
        except Exception as e:
86
            logger.warning('PsUtil can not grab processes io_counters ({})'.format(e))
87
            self.disable_io_counters = True
88
        else:
89
            logger.debug('PsUtil can grab processes io_counters')
90
            self.disable_io_counters = False
91
92
        # Test if the system can grab gids
93
        try:
94
            p = psutil.Process()
95
            p.gids()
96
        except Exception as e:
97
            logger.warning('PsUtil can not grab processes gids ({})'.format(e))
98
            self.disable_gids = True
99
        else:
100
            logger.debug('PsUtil can grab processes gids')
101
            self.disable_gids = False
102
103
        # Maximum number of processes showed in the UI (None if no limit)
104
        self._max_processes = None
105
106
        # Process filter
107
        self._filter = GlancesFilter()
108
109
        # Whether or not to hide kernel threads
110
        self.no_kernel_threads = False
111
112
        # Store maximums values in a dict
113
        # Used in the UI to highlight the maximum value
114
        self._max_values_list = ('cpu_percent', 'memory_percent')
115
        # { 'cpu_percent': 0.0, 'memory_percent': 0.0 }
116
        self._max_values = {}
117
        self.reset_max_values()
118
119
    def set_args(self, args):
120
        """Set args."""
121
        self.args = args
122
123
    def reset_processcount(self):
124
        """Reset the global process count"""
125
        self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0, 'pid_max': None}
126
127
    def update_processcount(self, plist):
128
        """Update the global process count from the current processes list"""
129
        # Update the maximum process ID (pid) number
130
        self.processcount['pid_max'] = self.pid_max
131
        # For each key in the processcount dict
132
        # count the number of processes with the same status
133
        for k in iterkeys(self.processcount):
134
            self.processcount[k] = len(list(filter(lambda v: v['status'] is k, plist)))
0 ignored issues
show
introduced by
The variable k does not seem to be defined in case the for loop on line 133 is not entered. Are you sure this can never be the case?
Loading history...
135
        # Compute thread
136
        self.processcount['thread'] = sum(i['num_threads'] for i in plist if i['num_threads'] is not None)
137
        # Compute total
138
        self.processcount['total'] = len(plist)
139
140
    def enable(self):
141
        """Enable process stats."""
142
        self.disable_tag = False
143
        self.update()
144
145
    def disable(self):
146
        """Disable process stats."""
147
        self.disable_tag = True
148
149
    def enable_extended(self):
150
        """Enable extended process stats."""
151
        self.disable_extended_tag = False
152
        self.update()
153
154
    def disable_extended(self):
155
        """Disable extended process stats."""
156
        self.disable_extended_tag = True
157
158
    @property
159
    def pid_max(self):
160
        """
161
        Get the maximum PID value.
162
163
        On Linux, the value is read from the `/proc/sys/kernel/pid_max` file.
164
165
        From `man 5 proc`:
166
        The default value for this file, 32768, results in the same range of
167
        PIDs as on earlier kernels. On 32-bit platforms, 32768 is the maximum
168
        value for pid_max. On 64-bit systems, pid_max can be set to any value
169
        up to 2^22 (PID_MAX_LIMIT, approximately 4 million).
170
171
        If the file is unreadable or not available for whatever reason,
172
        returns None.
173
174
        Some other OSes:
175
        - On FreeBSD and macOS the maximum is 99999.
176
        - On OpenBSD >= 6.0 the maximum is 99999 (was 32766).
177
        - On NetBSD the maximum is 30000.
178
179
        :returns: int or None
180
        """
181
        if LINUX:
182
            # XXX: waiting for https://github.com/giampaolo/psutil/issues/720
183
            try:
184
                with open('/proc/sys/kernel/pid_max', 'rb') as f:
185
                    return int(f.read())
186
            except (OSError, IOError):
187
                return None
188
        else:
189
            return None
190
191
    @property
192
    def processes_count(self):
193
        """Get the current number of processes showed in the UI."""
194
        return min(self._max_processes - 2, glances_processes.processcount['total'] - 1)
195
196
    @property
197
    def max_processes(self):
198
        """Get the maximum number of processes showed in the UI."""
199
        return self._max_processes
200
201
    @max_processes.setter
202
    def max_processes(self, value):
203
        """Set the maximum number of processes showed in the UI."""
204
        self._max_processes = value
205
206
    # Process filter
207
208
    @property
209
    def process_filter_input(self):
210
        """Get the process filter (given by the user)."""
211
        return self._filter.filter_input
212
213
    @property
214
    def process_filter(self):
215
        """Get the process filter (current apply filter)."""
216
        return self._filter.filter
217
218
    @process_filter.setter
219
    def process_filter(self, value):
220
        """Set the process filter."""
221
        self._filter.filter = value
222
223
    @property
224
    def process_filter_key(self):
225
        """Get the process filter key."""
226
        return self._filter.filter_key
227
228
    @property
229
    def process_filter_re(self):
230
        """Get the process regular expression compiled."""
231
        return self._filter.filter_re
232
233
    # Export filter
234
235
    @property
236
    def export_process_filter(self):
237
        """Get the export process filter (current export process filter list)."""
238
        return self._filter_export.filter
239
240
    @export_process_filter.setter
241
    def export_process_filter(self, value):
242
        """Set the export process filter list."""
243
        self._filter_export.filter = value
244
245
    # Kernel threads
246
247
    def disable_kernel_threads(self):
248
        """Ignore kernel threads in process list."""
249
        self.no_kernel_threads = True
250
251
    @property
252
    def sort_reverse(self):
253
        """Return True to sort processes in reverse 'key' order, False instead."""
254
        if self.sort_key == 'name' or self.sort_key == 'username':
255
            return False
256
257
        return True
258
259
    def max_values(self):
260
        """Return the max values dict."""
261
        return self._max_values
262
263
    def get_max_values(self, key):
264
        """Get the maximum values of the given stat (key)."""
265
        return self._max_values[key]
266
267
    def set_max_values(self, key, value):
268
        """Set the maximum value for a specific stat (key)."""
269
        self._max_values[key] = value
270
271
    def reset_max_values(self):
272
        """Reset the maximum values dict."""
273
        self._max_values = {}
274
        for k in self._max_values_list:
275
            self._max_values[k] = 0.0
276
277
    def get_extended_stats(self, proc):
278
        """Get the extended stats for the given PID."""
279
        # - cpu_affinity (Linux, Windows, FreeBSD)
280
        # - ionice (Linux and Windows > Vista)
281
        # - num_ctx_switches (not available on Illumos/Solaris)
282
        # - num_fds (Unix-like)
283
        # - num_handles (Windows)
284
        # - memory_maps (only swap, Linux)
285
        #   https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
286
        # - connections (TCP and UDP)
287
        # - CPU min/max/mean
288
289
        # Set the extended stats list (OS dependant)
290
        extended_stats = ['cpu_affinity', 'ionice', 'num_ctx_switches']
291
        if LINUX:
292
            # num_fds only available on Unix system (see issue #1351)
293
            extended_stats += ['num_fds']
294
        if WINDOWS:
295
            extended_stats += ['num_handles']
296
297
        ret = {}
298
        try:
299
            logger.debug('Grab extended stats for process {}'.format(proc['pid']))
300
301
            # Get PID of the selected process
302
            selected_process = psutil.Process(proc['pid'])
303
304
            # Get the extended stats for the selected process
305
            ret = selected_process.as_dict(attrs=extended_stats, ad_value=None)
306
307
            # Get memory swap for the selected process (Linux Only)
308
            ret['memory_swap'] = self.__get_extended_memory_swap(selected_process)
309
310
            # Get number of TCP and UDP network connections for the selected process
311
            ret['tcp'], ret['udp'] = self.__get_extended_connections(selected_process)
312
        except (psutil.NoSuchProcess, ValueError, AttributeError) as e:
313
            logger.error('Can not grab extended stats ({})'.format(e))
314
            self.extended_process = None
315
            ret['extended_stats'] = False
316
        else:
317
            # Compute CPU and MEM min/max/mean
318
            # Merge the returned dict with the current on
319
            ret.update(self.__get_min_max_mean(proc))
320
            self.extended_process = ret
321
            ret['extended_stats'] = True
322
        return namedtuple_to_dict(ret)
323
324
    def __get_min_max_mean(self, proc, prefix=['cpu', 'memory']):
325
        """Return the min/max/mean for the given process"""
326
        ret = {}
327
        for stat_prefix in prefix:
328
            min_key = stat_prefix + '_min'
329
            max_key = stat_prefix + '_max'
330
            mean_sum_key = stat_prefix + '_mean_sum'
331
            mean_counter_key = stat_prefix + '_mean_counter'
332
            if min_key not in self.extended_process:
333
                ret[min_key] = proc[stat_prefix + '_percent']
334
            else:
335
                ret[min_key] = min(proc[stat_prefix + '_percent'], self.extended_process[min_key])
336
            if max_key not in self.extended_process:
337
                ret[max_key] = proc[stat_prefix + '_percent']
338
            else:
339
                ret[max_key] = max(proc[stat_prefix + '_percent'], self.extended_process[max_key])
340
            if mean_sum_key not in self.extended_process:
341
                ret[mean_sum_key] = proc[stat_prefix + '_percent']
342
            else:
343
                ret[mean_sum_key] = self.extended_process[mean_sum_key] + proc[stat_prefix + '_percent']
344
            if mean_counter_key not in self.extended_process:
345
                ret[mean_counter_key] = 1
346
            else:
347
                ret[mean_counter_key] = self.extended_process[mean_counter_key] + 1
348
            ret[stat_prefix + '_mean'] = ret[mean_sum_key] / ret[mean_counter_key]
349
        return ret
350
351
    def __get_extended_memory_swap(self, process):
352
        """Return the memory swap for the given process"""
353
        if not LINUX:
354
            return None
355
        try:
356
            memory_swap = sum([v.swap for v in process.memory_maps()])
357
        except (psutil.NoSuchProcess, KeyError):
358
            # (KeyError catch for issue #1551)
359
            pass
360
        except (psutil.AccessDenied, NotImplementedError):
361
            # NotImplementedError: /proc/${PID}/smaps file doesn't exist
362
            # on kernel < 2.6.14 or CONFIG_MMU kernel configuration option
363
            # is not enabled (see psutil #533/glances #413).
364
            memory_swap = None
365
        return memory_swap
366
367
    def __get_extended_connections(self, process):
368
        """Return a tuple with (tcp, udp) connections count
369
        The code is compliant with both PsUtil<6 and Psutil>=6
370
        """
371
        try:
372
            # Hack for issue #2754 (PsUtil 6+)
373
            if psutil_version_info[0] >= 6:
374
                tcp = len(process.net_connections(kind="tcp"))
375
                udp = len(process.net_connections(kind="udp"))
376
            else:
377
                tcp = len(process.connections(kind="tcp"))
378
                udp = len(process.connections(kind="udp"))
379
        except (psutil.AccessDenied, psutil.NoSuchProcess):
380
            # Manage issue1283 (psutil.AccessDenied)
381
            tcp = None
382
            udp = None
383
        return tcp, udp
384
385
    def is_selected_extended_process(self, position):
386
        """Return True if the process is the selected one for extended stats."""
387
        return (
388
            hasattr(self.args, 'programs') and
389
            not self.args.programs and
390
            hasattr(self.args, 'enable_process_extended') and
391
            self.args.enable_process_extended and
392
            not self.disable_extended_tag and
393
            hasattr(self.args, 'cursor_position') and
394
            position == self.args.cursor_position and
395
            not self.args.disable_cursor
396
        )
397
398
    def update(self):
399
        """Update the processes stats."""
400
        # Init new processes stats
401
        processlist = []
402
403
        # Do not process if disable tag is set
404
        if self.disable_tag:
405
            return processlist
406
407
        # Time since last update (for disk_io rate computation)
408
        time_since_update = getTimeSinceLastUpdate('process_disk')
409
410
        # Grab standard stats
411
        #####################
412
        sorted_attrs = ['cpu_percent', 'cpu_times', 'memory_percent', 'name', 'status', 'num_threads']
413
        displayed_attr = ['memory_info', 'nice', 'pid']
414
        # The following attributes are cached and only retreive every self.cache_timeout seconds
415
        # Warning: 'name' can not be cached because it is used for filtering
416
        cached_attrs = ['cmdline', 'username']
417
418
        # Some stats are optional
419
        if not self.disable_io_counters:
420
            sorted_attrs.append('io_counters')
421
        if not self.disable_gids:
422
            displayed_attr.append('gids')
423
        # Some stats are not sort key
424
        # An optimisation can be done be only grabbed displayed_attr
425
        # for displayed processes (but only in standalone mode...)
426
        sorted_attrs.extend(displayed_attr)
427
        # Some stats are cached (not necessary to be refreshed every time)
428
        if self.cache_timer.finished():
429
            sorted_attrs += cached_attrs
430
            self.cache_timer.set(self.cache_timeout)
431
            self.cache_timer.reset()
432
            is_cached = False
433
        else:
434
            is_cached = True
435
436
        # Build the processes stats list (it is why we need psutil>=5.3.0)
437
        # This is one of the main bottleneck of Glances (see flame graph)
438
        # It may be optimized with PsUtil 6+ (see issue #2755)
439
        processlist = list(
440
            filter(
441
                lambda p: not (BSD and p.info['name'] == 'idle')
442
                and not (WINDOWS and p.info['name'] == 'System Idle Process')
443
                and not (MACOS and p.info['name'] == 'kernel_task')
444
                and not (self.no_kernel_threads and LINUX and p.info['gids'].real == 0),
445
                psutil.process_iter(attrs=sorted_attrs, ad_value=None),
446
            )
447
        )
448
        # Only get the info key
449
        processlist = [p.info for p in processlist]
450
        # Sort the processes list by the current sort_key
451
        processlist = sort_stats(processlist, sorted_by=self.sort_key, reverse=True)
452
453
        # Update the processcount
454
        self.update_processcount(processlist)
455
456
        # Loop over processes and :
457
        # - add extended stats for selected process
458
        # - add metadata
459
        for position, proc in enumerate(processlist):
460
            # Extended stats
461
            ################
462
463
            # Get the selected process when the 'e' key is pressed
464
            if self.is_selected_extended_process(position):
465
                self.extended_process = proc
466
467
            # Grab extended stats only for the selected process (see issue #2225)
468
            if self.extended_process is not None and proc['pid'] == self.extended_process['pid']:
469
                proc.update(self.get_extended_stats(self.extended_process))
470
                self.extended_process = namedtuple_to_dict(proc)
471
472
            # Meta data
473
            ###########
474
475
            # PID is the key
476
            proc['key'] = 'pid'
477
478
            # Time since last update (for disk_io rate computation)
479
            proc['time_since_update'] = time_since_update
480
481
            # Process status (only keep the first char)
482
            proc['status'] = str(proc['status'])[:1].upper()
483
484
            # Process IO
485
            # procstat['io_counters'] is a list:
486
            # [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
487
            # If io_tag = 0 > Access denied or first time (display "?")
488
            # If io_tag = 1 > No access denied (display the IO rate)
489
            if 'io_counters' in proc and proc['io_counters'] is not None:
490
                io_new = [proc['io_counters'][2], proc['io_counters'][3]]
491
                # For IO rate computation
492
                # Append saved IO r/w bytes
493
                try:
494
                    proc['io_counters'] = io_new + self.io_old[proc['pid']]
495
                    io_tag = 1
496
                except KeyError:
497
                    proc['io_counters'] = io_new + [0, 0]
498
                    io_tag = 0
499
                # then save the IO r/w bytes
500
                self.io_old[proc['pid']] = io_new
501
            else:
502
                proc['io_counters'] = [0, 0] + [0, 0]
503
                io_tag = 0
504
            # Append the IO tag (for display)
505
            proc['io_counters'] += [io_tag]
506
507
            # Manage cached information
508
            if is_cached:
509
                # Grab cached values (in case of a new incoming process)
510
                if proc['pid'] not in self.processlist_cache:
511
                    try:
512
                        self.processlist_cache[proc['pid']] = psutil.Process(pid=proc['pid']).as_dict(
513
                            attrs=cached_attrs, ad_value=None
514
                        )
515
                    except psutil.NoSuchProcess:
516
                        pass
517
                # Add cached value to current stat
518
                try:
519
                    proc.update(self.processlist_cache[proc['pid']])
520
                except KeyError:
521
                    pass
522
            else:
523
                # Save values to cache
524
                try:
525
                    self.processlist_cache[proc['pid']] = {cached: proc[cached] for cached in cached_attrs}
526
                except KeyError:
527
                    pass
528
529
        # Filter and transform process export list
530
        self.processlist_export = self.update_export_list(processlist)
531
532
        # Filter and transform process list
533
        processlist = self.update_list(processlist)
534
535
        # Compute the maximum value for keys in self._max_values_list: CPU, MEM
536
        # Useful to highlight the processes with maximum values
537
        for k in self._max_values_list:
538
            values_list = [i[k] for i in processlist if i[k] is not None]
539
            if values_list:
540
                self.set_max_values(k, max(values_list))
541
542
        # Update the stats
543
        self.processlist = processlist
544
545
        return self.processlist
546
547
    def update_list(self, processlist):
548
        """Return the process list after filtering and transformation (namedtuple to dict)."""
549
        if self._filter.filter is None:
550
            return list_of_namedtuple_to_list_of_dict(processlist)
551
        ret = list(filter(lambda p: self._filter.is_filtered(p),
552
                          processlist))
553
        return list_of_namedtuple_to_list_of_dict(ret)
554
555
    def update_export_list(self, processlist):
556
        """Return the process export list after filtering and transformation (namedtuple to dict)."""
557
        if self._filter_export.filter == []:
558
            return []
559
        ret = list(filter(lambda p: self._filter_export.is_filtered(p),
560
                          processlist))
561
        return list_of_namedtuple_to_list_of_dict(ret)
562
563
    def get_count(self):
564
        """Get the number of processes."""
565
        return self.processcount
566
567
    def get_list(self, sorted_by=None, as_programs=False):
568
        """Get the processlist.
569
        By default, return the list of threads.
570
        If as_programs is True, return the list of programs."""
571
        if as_programs:
572
            return processes_to_programs(self.processlist)
573
        else:
574
            return self.processlist
575
576
    def get_export(self):
577
        """Return the processlist for export."""
578
        return self.processlist_export
579
580
    @property
581
    def sort_key(self):
582
        """Get the current sort key."""
583
        return self._sort_key
584
585
    def set_sort_key(self, key, auto=True):
586
        """Set the current sort key."""
587
        if key == 'auto':
588
            self.auto_sort = True
589
            self._sort_key = 'cpu_percent'
590
        else:
591
            self.auto_sort = auto
592
            self._sort_key = key
593
594
    def nice_decrease(self, pid):
595
        """Decrease nice level
596
        On UNIX this is a number which usually goes from -20 to 20.
597
        The higher the nice value, the lower the priority of the process."""
598
        p = psutil.Process(pid)
599
        try:
600
            p.nice(p.nice() - 1)
601
            logger.info('Set nice level of process {} to {} (higher the priority)'.format(pid, p.nice()))
602
        except psutil.AccessDenied:
603
            logger.warning(
604
                'Can not decrease (higher the priority) the nice level of process {} (access denied)'.format(pid)
605
            )
606
607
    def nice_increase(self, pid):
608
        """Increase nice level
609
        On UNIX this is a number which usually goes from -20 to 20.
610
        The higher the nice value, the lower the priority of the process."""
611
        p = psutil.Process(pid)
612
        try:
613
            p.nice(p.nice() + 1)
614
            logger.info('Set nice level of process {} to {} (lower the priority)'.format(pid, p.nice()))
615
        except psutil.AccessDenied:
616
            logger.warning(
617
                'Can not increase (lower the priority) the nice level of process {} (access denied)'.format(pid)
618
            )
619
620
    def kill(self, pid, timeout=3):
621
        """Kill process with pid"""
622
        assert pid != os.getpid(), "Glances can kill itself..."
623
        p = psutil.Process(pid)
624
        logger.debug('Send kill signal to process: {}'.format(p))
625
        p.kill()
626
        return p.wait(timeout)
627
628
629
def weighted(value):
630
    """Manage None value in dict value."""
631
    return -float('inf') if value is None else value
632
633
634
def _sort_io_counters(process, sorted_by='io_counters', sorted_by_secondary='memory_percent'):
635
    """Specific case for io_counters
636
637
    :return: Sum of io_r + io_w
638
    """
639
    return process[sorted_by][0] - process[sorted_by][2] + process[sorted_by][1] - process[sorted_by][3]
640
641
642
def _sort_cpu_times(process, sorted_by='cpu_times', sorted_by_secondary='memory_percent'):
643
    """Specific case for cpu_times
644
645
    Patch for "Sorting by process time works not as expected #1321"
646
    By default PsUtil only takes user time into account
647
    see (https://github.com/giampaolo/psutil/issues/1339)
648
    The following implementation takes user and system time into account
649
    """
650
    return process[sorted_by][0] + process[sorted_by][1]
651
652
653
def _sort_lambda(sorted_by='cpu_percent', sorted_by_secondary='memory_percent'):
654
    """Return a sort lambda function for the sorted_by key"""
655
    ret = None
656
    if sorted_by == 'io_counters':
657
        ret = _sort_io_counters
658
    elif sorted_by == 'cpu_times':
659
        ret = _sort_cpu_times
660
    return ret
661
662
663
def sort_stats(stats, sorted_by='cpu_percent', sorted_by_secondary='memory_percent', reverse=True):
664
    """Return the stats (dict) sorted by (sorted_by).
665
    A secondary sort key should be specified.
666
667
    Reverse the sort if reverse is True.
668
    """
669
    if sorted_by is None and sorted_by_secondary is None:
670
        # No need to sort...
671
        return stats
672
673
    # Check if a specific sort should be done
674
    sort_lambda = _sort_lambda(sorted_by=sorted_by, sorted_by_secondary=sorted_by_secondary)
675
676
    if sort_lambda is not None:
677
        # Specific sort
678
        try:
679
            stats.sort(key=sort_lambda, reverse=reverse)
680
        except Exception:
681
            # If an error is detected, fallback to cpu_percent
682
            stats.sort(
683
                key=lambda process: (weighted(process['cpu_percent']), weighted(process[sorted_by_secondary])),
684
                reverse=reverse,
685
            )
686
    else:
687
        # Standard sort
688
        try:
689
            stats.sort(
690
                key=lambda process: (weighted(process[sorted_by]), weighted(process[sorted_by_secondary])),
691
                reverse=reverse,
692
            )
693
        except (KeyError, TypeError):
694
            # Fallback to name
695
            stats.sort(key=lambda process: process['name'] if process['name'] is not None else '~', reverse=False)
696
697
    return stats
698
699
700
glances_processes = GlancesProcesses()
701