diff --git a/src/pcp/ps/pcp-ps.py b/src/pcp/ps/pcp-ps.py index 2764c0e2c1a..8b695ff5394 100755 --- a/src/pcp/ps/pcp-ps.py +++ b/src/pcp/ps/pcp-ps.py @@ -39,6 +39,12 @@ SCHED_POLICY = ['NORMAL', 'FIFO', 'RR', 'BATCH', '', 'IDLE', 'DEADLINE'] +def needs_previous_values(options): + if options.selective_colum_flag: + return '%cpu' in options.column_list + return options.universal_flag in ('user', 'username') + + class StdoutPrinter: def Print(self, args): print(args) @@ -281,11 +287,11 @@ def start(self): def total_time(self): - c_usertime = self.__get_value('proc.psinfo.stime', self.instance) - p_guesttime = self.__get_previous_value('proc.psinfo.utime', self.instance) + c_usertime = self.__get_value('proc.psinfo.utime', self.instance) + c_systemtime = self.__get_value('proc.psinfo.stime', self.instance) timefmt = "%H:%M:%S" - if c_usertime and p_guesttime is not None: - total_time = (c_usertime / 1000) + (p_guesttime / 1000) + if c_usertime is not None and c_systemtime is not None: + total_time = (c_usertime + c_systemtime) / 1000 else: total_time = 0 return time.strftime(timefmt, time.gmtime(total_time)) @@ -456,7 +462,7 @@ def print_report(self, timestamp, header_indentation, value_indentation): "pid": "Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD", "ppid": "Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD", "username": "Timestamp" + header_indentation + "USERNAME\t\tPID\t\t%CPU\t%MEM\tVSZ\tRSS\t" + - "TTY\tSTAT\t\tTIME\t\tSTART\t\tCOMMAND", + "TTY\tSTAT\t\tTIME\t\tSTART\t\tCOMMAND", "command": "Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD" } selected_flag = self.processStatOptions.universal_flag @@ -526,14 +532,14 @@ def safe_str(val): "please remove sorting flag or choose another flag for output") if self.processStatOptions.sorting_order == '%cpu': output_rows.sort( - key=lambda x: float(x.split()[cpu_idx]) if x.split()[cpu_idx].replace('.', '', 1).isdigit() - else float('-inf'), - reverse=True) + key=lambda x: float(x.split()[cpu_idx]) if x.split()[cpu_idx].replace('.', '', 1).isdigit() + else float('-inf'), + reverse=True) elif self.processStatOptions.sorting_order == '%mem': output_rows.sort( - key=lambda x: float(x.split()[mem_idx]) if x.split()[mem_idx].replace('.', '', 1).isdigit() - else float('-inf'), - reverse=True) + key=lambda x: float(x.split()[mem_idx]) if x.split()[mem_idx].replace('.', '', 1).isdigit() + else float('-inf'), + reverse=True) if output_rows: self.printer(header) self.printer('\n'.join(output_rows)) @@ -576,8 +582,9 @@ def __print_report(self, manager,timestamp, header_indentation, value_indentatio process_filter = ProcessFilter(self.processStatOptions) stdout = StdoutPrinter() printdecorator = NoneHandlingPrinterDecorator(stdout) - report = ProcessStatusReporter(process_report, process_filter, interval_in_seconds, - printdecorator.Print, self.processStatOptions) + report = ProcessStatusReporter( + process_report, process_filter, interval_in_seconds, + printdecorator.Print, self.processStatOptions) report.print_report(timestamp, header_indentation, value_indentation) def __print_dynamic_report(self, manager,timestamp, header_indentation, value_indentation,interval_in_seconds): if self.processStatOptions.debug_mode: @@ -597,7 +604,8 @@ def __get_timestamp(self): def report(self, manager): try: - if self.group['proc.psinfo.utime'].netPrevValues is None: + if (needs_previous_values(self.processStatOptions) and + self.group['proc.psinfo.stime'].netPrevValues is None): return False # Not ready, skip increment if not self.group['hinv.ncpu'].netValues or not self.group['kernel.uname.sysname'].netValues: return False @@ -612,12 +620,11 @@ def report(self, manager): if self.processStatOptions.debug_mode: print("Starting report generation") print("Need to print samples: %s" % self.processStatOptions.print_count) - if self.processStatOptions.print_count == 0: - if self.processStatOptions.debug_mode: - print("Print count exhausted, exiting") - sys.exit(0) timestamp = self.__get_timestamp() - interval_in_seconds = self.timeStampDelta() + try: + interval_in_seconds = self.timeStampDelta() + except AttributeError: + interval_in_seconds = 0 header_indentation = " " if len(timestamp) < 9 else (len(timestamp) - 7) * " " value_indentation = ((len(header_indentation) + 9) - len(timestamp)) * " " @@ -635,6 +642,8 @@ def report(self, manager): self.__print_report(manager,timestamp, header_indentation, value_indentation, interval_in_seconds) if self.processStatOptions.context is not PM_CONTEXT_ARCHIVE: self.processStatOptions.print_count -= 1 + if self.processStatOptions.print_count == 0: + sys.exit(0) return True # Data was printed finally: sys.stdout.flush() @@ -733,13 +742,11 @@ def options(self): self.pmSetLongOptionText( "\twchan\tWCHAN \tname of the kernel function in which the process is sleeping" ) - self.pmSetLongOption("", 0, 'u', "", - "Display user-oriented format" - ) + self.pmSetLongOption( + "", 0, 'u', "", "Display user-oriented format") self.pmSetLongOption( "sort", 1, "O", "%cpu,%mem", - "sort the process list by %cpu or %mem values " - ) + "sort the process list by %cpu or %mem values ") self.pmSetLongOption("", 0, "d", "", "enable debug mode") self.pmSetLongOptionVersion() self.pmSetLongOptionTimeZone() @@ -893,6 +900,11 @@ def checkOptions(self): opts = ProcessStatOptions() manager = pmcc.MetricGroupManager.builder(opts, sys.argv) ProcessStatOptions.context = manager.type + if manager.type is PM_CONTEXT_ARCHIVE and needs_previous_values(opts): + samples = opts.pmGetOptionSamples() + if samples is not None: + # The manager's initial archive fetch precedes its report loop. + opts.pmSetOptionSamples(str(samples + 1)) if not opts.checkOptions(): raise pmapi.pmUsageErr missing = manager.checkMissingMetrics(PSSTAT_METRICS) diff --git a/src/pcp/ps/test/process_stat_report_test.py b/src/pcp/ps/test/process_stat_report_test.py new file mode 100644 index 00000000000..ae9f08fb7e8 --- /dev/null +++ b/src/pcp/ps/test/process_stat_report_test.py @@ -0,0 +1,72 @@ +#!/usr/bin/env pmpython + +import unittest +from unittest.mock import ANY, MagicMock, Mock, patch + +from pcp_ps import PM_CONTEXT_ARCHIVE, ProcessStatReport, needs_previous_values + + +class TestProcessStatReport(unittest.TestCase): + def _report(self, options, utime_previous_values, stime_previous_values): + group = MagicMock() + utime = MagicMock(netPrevValues=utime_previous_values, netValues=[object()]) + stime = MagicMock(netPrevValues=stime_previous_values, netValues=[object()]) + metric = MagicMock(netValues=[object()]) + group.__getitem__.side_effect = lambda name: { + 'proc.psinfo.utime': utime, + 'proc.psinfo.stime': stime, + }.get(name, metric) + report = ProcessStatReport(group, options) + report.Machine_info_count = 1 + return report + + def test_default_report_does_not_wait_for_previous_values(self): + options = Mock(context=None, debug_mode=False, print_count=1, + selective_colum_flag=False, universal_flag='all') + report = self._report(options, None, None) + + with patch.object(report, '_ProcessStatReport__get_timestamp', return_value='00:00:00'), \ + patch.object(report, 'timeStampDelta', side_effect=AttributeError), \ + patch.object(report, '_ProcessStatReport__print_report') as print_report, \ + self.assertRaises(SystemExit): + report.report(Mock()) + + print_report.assert_called_once_with(ANY, '00:00:00', ' ', ' ', 0) + + def test_user_report_waits_for_previous_values(self): + options = Mock(context=None, debug_mode=False, print_count=1, + selective_colum_flag=False, universal_flag='user') + report = self._report(options, object(), None) + + self.assertFalse(report.report(Mock())) + + def test_archive_report_does_not_decrement_print_count(self): + options = Mock(context=PM_CONTEXT_ARCHIVE, debug_mode=False, + print_count=1, selective_colum_flag=False, + universal_flag='all') + report = self._report(options, object(), object()) + + with patch.object(report, '_ProcessStatReport__get_timestamp', return_value='00:00:00'), \ + patch.object(report, 'timeStampDelta', return_value=1.0), \ + patch.object(report, '_ProcessStatReport__print_report') as print_report: + self.assertTrue(report.report(Mock())) + + self.assertEqual(options.print_count, 1) + print_report.assert_called_once() + + def test_previous_values_are_only_needed_for_cpu_reports(self): + default = Mock(universal_flag='all', selective_colum_flag=False) + user = Mock(universal_flag='user', selective_colum_flag=False) + columns = Mock(universal_flag='all', selective_colum_flag=True, + column_list=['pid', '%cpu']) + non_cpu_columns = Mock(universal_flag='user', selective_colum_flag=True, + column_list=['pid', 'args']) + + self.assertFalse(needs_previous_values(default)) + self.assertTrue(needs_previous_values(user)) + self.assertTrue(needs_previous_values(columns)) + self.assertFalse(needs_previous_values(non_cpu_columns)) + + +if __name__ == '__main__': + unittest.main()