diff options
Diffstat (limited to 'crosperf/results_cache.py')
-rw-r--r-- | crosperf/results_cache.py | 2997 |
1 files changed, 1595 insertions, 1402 deletions
diff --git a/crosperf/results_cache.py b/crosperf/results_cache.py index 5525858c..043da990 100644 --- a/crosperf/results_cache.py +++ b/crosperf/results_cache.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +# Copyright 2013 The ChromiumOS Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Module to deal with result cache.""" -from __future__ import division -from __future__ import print_function import collections import glob @@ -20,642 +18,729 @@ import tempfile from cros_utils import command_executer from cros_utils import misc - from image_checksummer import ImageChecksummer - import results_report import test_flag -SCRATCH_DIR = os.path.expanduser('~/cros_scratch') -RESULTS_FILE = 'results.pickle' -MACHINE_FILE = 'machine.txt' -AUTOTEST_TARBALL = 'autotest.tbz2' -RESULTS_TARBALL = 'results.tbz2' -PERF_RESULTS_FILE = 'perf-results.txt' -CACHE_KEYS_FILE = 'cache_keys.txt' + +SCRATCH_DIR = os.path.expanduser("~/cros_scratch") +RESULTS_FILE = "results.pickle" +MACHINE_FILE = "machine.txt" +AUTOTEST_TARBALL = "autotest.tbz2" +RESULTS_TARBALL = "results.tbz2" +PERF_RESULTS_FILE = "perf-results.txt" +CACHE_KEYS_FILE = "cache_keys.txt" class PidVerificationError(Exception): - """Error of perf PID verification in per-process mode.""" + """Error of perf PID verification in per-process mode.""" class PerfDataReadError(Exception): - """Error of reading a perf.data header.""" + """Error of reading a perf.data header.""" class Result(object): - """Class for holding the results of a single test run. - - This class manages what exactly is stored inside the cache without knowing - what the key of the cache is. For runs with perf, it stores perf.data, - perf.report, etc. The key generation is handled by the ResultsCache class. - """ - - def __init__(self, logger, label, log_level, machine, cmd_exec=None): - self.chromeos_root = label.chromeos_root - self._logger = logger - self.ce = cmd_exec or command_executer.GetCommandExecuter( - self._logger, log_level=log_level) - self.temp_dir = None - self.label = label - self.results_dir = None - self.log_level = log_level - self.machine = machine - self.perf_data_files = [] - self.perf_report_files = [] - self.results_file = [] - self.turbostat_log_file = '' - self.cpustats_log_file = '' - self.cpuinfo_file = '' - self.top_log_file = '' - self.wait_time_log_file = '' - self.chrome_version = '' - self.err = None - self.chroot_results_dir = '' - self.test_name = '' - self.keyvals = None - self.board = None - self.suite = None - self.cwp_dso = '' - self.retval = None - self.out = None - self.top_cmds = [] - - def GetTopCmds(self): - """Get the list of top commands consuming CPU on the machine.""" - return self.top_cmds - - def FormatStringTopCommands(self): - """Get formatted string of top commands. - - Get the formatted string with top commands consuming CPU on DUT machine. - Number of "non-chrome" processes in the list is limited to 5. - """ - format_list = [ - 'Top commands with highest CPU usage:', - # Header. - '%20s %9s %6s %s' % ('COMMAND', 'AVG CPU%', 'COUNT', 'HIGHEST 5'), - '-' * 50, - ] - if self.top_cmds: - # After switching to top processes we have to expand the list since there - # will be a lot of 'chrome' processes (up to 10, sometimes more) in the - # top. - # Let's limit the list size by the number of non-chrome processes. - limit_of_non_chrome_procs = 5 - num_of_non_chrome_procs = 0 - for topcmd in self.top_cmds: - print_line = '%20s %9.2f %6s %s' % ( - topcmd['cmd'], topcmd['cpu_use_avg'], topcmd['count'], - topcmd['top5_cpu_use']) - format_list.append(print_line) - if not topcmd['cmd'].startswith('chrome'): - num_of_non_chrome_procs += 1 - if num_of_non_chrome_procs >= limit_of_non_chrome_procs: - break - else: - format_list.append('[NO DATA FROM THE TOP LOG]') - format_list.append('-' * 50) - return '\n'.join(format_list) - - def CopyFilesTo(self, dest_dir, files_to_copy): - file_index = 0 - for file_to_copy in files_to_copy: - if not os.path.isdir(dest_dir): - command = 'mkdir -p %s' % dest_dir - self.ce.RunCommand(command) - dest_file = os.path.join( - dest_dir, ('%s.%s' % (os.path.basename(file_to_copy), file_index))) - ret = self.ce.CopyFiles(file_to_copy, dest_file, recursive=False) - if ret: - raise IOError('Could not copy results file: %s' % file_to_copy) - file_index += 1 - - def CopyResultsTo(self, dest_dir): - self.CopyFilesTo(dest_dir, self.results_file) - self.CopyFilesTo(dest_dir, self.perf_data_files) - self.CopyFilesTo(dest_dir, self.perf_report_files) - extra_files = [] - if self.top_log_file: - extra_files.append(self.top_log_file) - if self.cpuinfo_file: - extra_files.append(self.cpuinfo_file) - if extra_files: - self.CopyFilesTo(dest_dir, extra_files) - if self.results_file or self.perf_data_files or self.perf_report_files: - self._logger.LogOutput('Results files stored in %s.' % dest_dir) - - def CompressResultsTo(self, dest_dir): - tarball = os.path.join(self.results_dir, RESULTS_TARBALL) - # Test_that runs hold all output under TEST_NAME_HASHTAG/results/, - # while tast runs hold output under TEST_NAME/. - # Both ensure to be unique. - result_dir_name = self.test_name if self.suite == 'tast' else 'results' - results_dir = self.FindFilesInResultsDir('-name %s' % - result_dir_name).split('\n')[0] - - if not results_dir: - self._logger.LogOutput('WARNING: No results dir matching %r found' % - result_dir_name) - return - - self.CreateTarball(results_dir, tarball) - self.CopyFilesTo(dest_dir, [tarball]) - if results_dir: - self._logger.LogOutput('Results files compressed into %s.' % dest_dir) - - def GetNewKeyvals(self, keyvals_dict): - # Initialize 'units' dictionary. - units_dict = {} - for k in keyvals_dict: - units_dict[k] = '' - results_files = self.GetDataMeasurementsFiles() - for f in results_files: - # Make sure we can find the results file - if os.path.exists(f): - data_filename = f - else: - # Otherwise get the base filename and create the correct - # path for it. - _, f_base = misc.GetRoot(f) - data_filename = os.path.join(self.chromeos_root, 'chroot/tmp', - self.temp_dir, f_base) - if data_filename.find('.json') > 0: - raw_dict = dict() - if os.path.exists(data_filename): - with open(data_filename, 'r') as data_file: - raw_dict = json.load(data_file) - - if 'charts' in raw_dict: - raw_dict = raw_dict['charts'] - for k1 in raw_dict: - field_dict = raw_dict[k1] - for k2 in field_dict: - result_dict = field_dict[k2] - key = k1 + '__' + k2 - if 'value' in result_dict: - keyvals_dict[key] = result_dict['value'] - elif 'values' in result_dict: - values = result_dict['values'] - if ('type' in result_dict - and result_dict['type'] == 'list_of_scalar_values' and values - and values != 'null'): - keyvals_dict[key] = sum(values) / float(len(values)) - else: - keyvals_dict[key] = values - units_dict[key] = result_dict['units'] - else: - if os.path.exists(data_filename): - with open(data_filename, 'r') as data_file: - lines = data_file.readlines() - for line in lines: - tmp_dict = json.loads(line) - graph_name = tmp_dict['graph'] - graph_str = (graph_name + '__') if graph_name else '' - key = graph_str + tmp_dict['description'] - keyvals_dict[key] = tmp_dict['value'] - units_dict[key] = tmp_dict['units'] - - return keyvals_dict, units_dict - - def AppendTelemetryUnits(self, keyvals_dict, units_dict): - """keyvals_dict is the dict of key-value used to generate Crosperf reports. - - units_dict is a dictionary of the units for the return values in - keyvals_dict. We need to associate the units with the return values, - for Telemetry tests, so that we can include the units in the reports. - This function takes each value in keyvals_dict, finds the corresponding - unit in the units_dict, and replaces the old value with a list of the - old value and the units. This later gets properly parsed in the - ResultOrganizer class, for generating the reports. - """ + """Class for holding the results of a single test run. - results_dict = {} - for k in keyvals_dict: - # We don't want these lines in our reports; they add no useful data. - if not k or k == 'telemetry_Crosperf': - continue - val = keyvals_dict[k] - units = units_dict[k] - new_val = [val, units] - results_dict[k] = new_val - return results_dict - - def GetKeyvals(self): - results_in_chroot = os.path.join(self.chromeos_root, 'chroot', 'tmp') - if not self.temp_dir: - self.temp_dir = tempfile.mkdtemp(dir=results_in_chroot) - command = f'cp -r {self.results_dir}/* {self.temp_dir}' - self.ce.RunCommand(command, print_to_console=False) - - command = ('./generate_test_report --no-color --csv %s' % - (os.path.join('/tmp', os.path.basename(self.temp_dir)))) - _, out, _ = self.ce.ChrootRunCommandWOutput(self.chromeos_root, - command, - print_to_console=False) - keyvals_dict = {} - tmp_dir_in_chroot = misc.GetInsideChrootPath(self.chromeos_root, - self.temp_dir) - for line in out.splitlines(): - tokens = re.split('=|,', line) - key = tokens[-2] - if key.startswith(tmp_dir_in_chroot): - key = key[len(tmp_dir_in_chroot) + 1:] - value = tokens[-1] - keyvals_dict[key] = value - - # Check to see if there is a perf_measurements file and get the - # data from it if so. - keyvals_dict, units_dict = self.GetNewKeyvals(keyvals_dict) - if self.suite == 'telemetry_Crosperf': - # For telemtry_Crosperf results, append the units to the return - # results, for use in generating the reports. - keyvals_dict = self.AppendTelemetryUnits(keyvals_dict, units_dict) - return keyvals_dict - - def GetSamples(self): - actual_samples = 0 - for perf_data_file in self.perf_data_files: - chroot_perf_data_file = misc.GetInsideChrootPath(self.chromeos_root, - perf_data_file) - perf_path = os.path.join(self.chromeos_root, 'chroot', 'usr/bin/perf') - perf_file = '/usr/sbin/perf' - if os.path.exists(perf_path): - perf_file = '/usr/bin/perf' - - # For each perf.data, we want to collect sample count for specific DSO. - # We specify exact match for known DSO type, and every sample for `all`. - exact_match = '' - if self.cwp_dso == 'all': - exact_match = '""' - elif self.cwp_dso == 'chrome': - exact_match = '" chrome "' - elif self.cwp_dso == 'kallsyms': - exact_match = '"[kernel.kallsyms]"' - else: - # This will need to be updated once there are more DSO types supported, - # if user want an exact match for the field they want. - exact_match = '"%s"' % self.cwp_dso - - command = ('%s report -n -s dso -i %s 2> /dev/null | grep %s' % - (perf_file, chroot_perf_data_file, exact_match)) - _, result, _ = self.ce.ChrootRunCommandWOutput(self.chromeos_root, - command) - # Accumulate the sample count for all matched fields. - # Each line looks like this: - # 45.42% 237210 chrome - # And we want the second number which is the sample count. - samples = 0 - try: - for line in result.split('\n'): - attr = line.split() - if len(attr) == 3 and '%' in attr[0]: - samples += int(attr[1]) - except: - raise RuntimeError('Cannot parse perf dso result') - - actual_samples += samples - - # Remove idle cycles from the accumulated sample count. - perf_report_file = f'{perf_data_file}.report' - if not os.path.exists(perf_report_file): - raise RuntimeError(f'Missing perf report file: {perf_report_file}') - - idle_functions = { - '[kernel.kallsyms]': - ('intel_idle', 'arch_cpu_idle', 'intel_idle', 'cpu_startup_entry', - 'default_idle', 'cpu_idle_loop', 'do_idle'), - } - idle_samples = 0 - - with open(perf_report_file) as f: - try: - for line in f: - line = line.strip() - if not line or line[0] == '#': - continue - # Each line has the following fields, - # pylint: disable=line-too-long - # Overhead Samples Command Shared Object Symbol - # pylint: disable=line-too-long - # 1.48% 60 swapper [kernel.kallsyms] [k] intel_idle - # pylint: disable=line-too-long - # 0.00% 1 shill libshill-net.so [.] std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >::vector<unsigned char const*> - _, samples, _, dso, _, function = line.split(None, 5) - - if dso in idle_functions and function in idle_functions[dso]: - if self.log_level != 'verbose': - self._logger.LogOutput('Removing %s samples from %s in %s' % - (samples, function, dso)) - idle_samples += int(samples) - except: - raise RuntimeError('Cannot parse perf report') - actual_samples -= idle_samples - return [actual_samples, u'samples'] - - def GetResultsDir(self): - if self.suite == 'tast': - mo = re.search(r'Writing results to (\S+)', self.out) - else: - mo = re.search(r'Results placed in (\S+)', self.out) - if mo: - result = mo.group(1) - return result - raise RuntimeError('Could not find results directory.') - - def FindFilesInResultsDir(self, find_args): - if not self.results_dir: - return '' - - command = 'find %s %s' % (self.results_dir, find_args) - ret, out, _ = self.ce.RunCommandWOutput(command, print_to_console=False) - if ret: - raise RuntimeError('Could not run find command!') - return out - - def GetResultsFile(self): - if self.suite == 'telemetry_Crosperf': - return self.FindFilesInResultsDir('-name histograms.json').splitlines() - return self.FindFilesInResultsDir('-name results-chart.json').splitlines() - - def GetPerfDataFiles(self): - return self.FindFilesInResultsDir('-name perf.data').splitlines() - - def GetPerfReportFiles(self): - return self.FindFilesInResultsDir('-name perf.data.report').splitlines() - - def GetDataMeasurementsFiles(self): - result = self.FindFilesInResultsDir('-name perf_measurements').splitlines() - if not result: - if self.suite == 'telemetry_Crosperf': - result = ( - self.FindFilesInResultsDir('-name histograms.json').splitlines()) - else: - result = (self.FindFilesInResultsDir( - '-name results-chart.json').splitlines()) - return result - - def GetTurbostatFile(self): - """Get turbostat log path string.""" - return self.FindFilesInResultsDir('-name turbostat.log').split('\n')[0] - - def GetCpustatsFile(self): - """Get cpustats log path string.""" - return self.FindFilesInResultsDir('-name cpustats.log').split('\n')[0] - - def GetCpuinfoFile(self): - """Get cpustats log path string.""" - return self.FindFilesInResultsDir('-name cpuinfo.log').split('\n')[0] - - def GetTopFile(self): - """Get cpustats log path string.""" - return self.FindFilesInResultsDir('-name top.log').split('\n')[0] - - def GetWaitTimeFile(self): - """Get wait time log path string.""" - return self.FindFilesInResultsDir('-name wait_time.log').split('\n')[0] - - def _CheckDebugPath(self, option, path): - relative_path = path[1:] - out_chroot_path = os.path.join(self.chromeos_root, 'chroot', relative_path) - if os.path.exists(out_chroot_path): - if option == 'kallsyms': - path = os.path.join(path, 'System.map-*') - return '--' + option + ' ' + path - else: - print('** WARNING **: --%s option not applied, %s does not exist' % - (option, out_chroot_path)) - return '' - - def GeneratePerfReportFiles(self): - perf_report_files = [] - for perf_data_file in self.perf_data_files: - # Generate a perf.report and store it side-by-side with the perf.data - # file. - chroot_perf_data_file = misc.GetInsideChrootPath(self.chromeos_root, - perf_data_file) - perf_report_file = '%s.report' % perf_data_file - if os.path.exists(perf_report_file): - raise RuntimeError('Perf report file already exists: %s' % - perf_report_file) - chroot_perf_report_file = misc.GetInsideChrootPath( - self.chromeos_root, perf_report_file) - perf_path = os.path.join(self.chromeos_root, 'chroot', 'usr/bin/perf') - - perf_file = '/usr/sbin/perf' - if os.path.exists(perf_path): - perf_file = '/usr/bin/perf' - - debug_path = self.label.debug_path - - if debug_path: - symfs = '--symfs ' + debug_path - vmlinux = '--vmlinux ' + os.path.join(debug_path, 'usr', 'lib', - 'debug', 'boot', 'vmlinux') - kallsyms = '' - print('** WARNING **: --kallsyms option not applied, no System.map-* ' - 'for downloaded image.') - else: - if self.label.image_type != 'local': - print('** WARNING **: Using local debug info in /build, this may ' - 'not match the downloaded image.') - build_path = os.path.join('/build', self.board) - symfs = self._CheckDebugPath('symfs', build_path) - vmlinux_path = os.path.join(build_path, 'usr/lib/debug/boot/vmlinux') - vmlinux = self._CheckDebugPath('vmlinux', vmlinux_path) - kallsyms_path = os.path.join(build_path, 'boot') - kallsyms = self._CheckDebugPath('kallsyms', kallsyms_path) - - command = ('%s report -n %s %s %s -i %s --stdio > %s' % - (perf_file, symfs, vmlinux, kallsyms, chroot_perf_data_file, - chroot_perf_report_file)) - if self.log_level != 'verbose': - self._logger.LogOutput('Generating perf report...\nCMD: %s' % command) - exit_code = self.ce.ChrootRunCommand(self.chromeos_root, command) - if exit_code == 0: - if self.log_level != 'verbose': - self._logger.LogOutput('Perf report generated successfully.') - else: - raise RuntimeError('Perf report not generated correctly. CMD: %s' % - command) - - # Add a keyval to the dictionary for the events captured. - perf_report_files.append( - misc.GetOutsideChrootPath(self.chromeos_root, - chroot_perf_report_file)) - return perf_report_files - - def GatherPerfResults(self): - report_id = 0 - for perf_report_file in self.perf_report_files: - with open(perf_report_file, 'r') as f: - report_contents = f.read() - for group in re.findall(r'Events: (\S+) (\S+)', report_contents): - num_events = group[0] - event_name = group[1] - key = 'perf_%s_%s' % (report_id, event_name) - value = str(misc.UnitToNumber(num_events)) - self.keyvals[key] = value - - def PopulateFromRun(self, out, err, retval, test, suite, cwp_dso): - self.board = self.label.board - self.out = out - self.err = err - self.retval = retval - self.test_name = test - self.suite = suite - self.cwp_dso = cwp_dso - self.chroot_results_dir = self.GetResultsDir() - self.results_dir = misc.GetOutsideChrootPath(self.chromeos_root, - self.chroot_results_dir) - self.results_file = self.GetResultsFile() - self.perf_data_files = self.GetPerfDataFiles() - # Include all perf.report data in table. - self.perf_report_files = self.GeneratePerfReportFiles() - self.turbostat_log_file = self.GetTurbostatFile() - self.cpustats_log_file = self.GetCpustatsFile() - self.cpuinfo_file = self.GetCpuinfoFile() - self.top_log_file = self.GetTopFile() - self.wait_time_log_file = self.GetWaitTimeFile() - # TODO(asharif): Do something similar with perf stat. - - # Grab keyvals from the directory. - self.ProcessResults() - - def ProcessChartResults(self): - # Open and parse the json results file generated by telemetry/test_that. - if not self.results_file: - raise IOError('No results file found.') - filename = self.results_file[0] - if not filename.endswith('.json'): - raise IOError('Attempt to call json on non-json file: %s' % filename) - if not os.path.exists(filename): - raise IOError('%s does not exist' % filename) - - keyvals = {} - with open(filename, 'r') as f: - raw_dict = json.load(f) - if 'charts' in raw_dict: - raw_dict = raw_dict['charts'] - for k, field_dict in raw_dict.items(): - for item in field_dict: - keyname = k + '__' + item - value_dict = field_dict[item] - if 'value' in value_dict: - result = value_dict['value'] - elif 'values' in value_dict: - values = value_dict['values'] - if not values: - continue - if ('type' in value_dict - and value_dict['type'] == 'list_of_scalar_values' - and values != 'null'): - result = sum(values) / float(len(values)) - else: - result = values - else: - continue - units = value_dict['units'] - new_value = [result, units] - keyvals[keyname] = new_value - return keyvals - - def ProcessTurbostatResults(self): - """Given turbostat_log_file non-null parse cpu stats from file. - - Returns: - Dictionary of 'cpufreq', 'cputemp' where each - includes dictionary 'all': [list_of_values] - - Example of the output of turbostat_log. - ---------------------- - CPU Avg_MHz Busy% Bzy_MHz TSC_MHz IRQ CoreTmp - - 329 12.13 2723 2393 10975 77 - 0 336 12.41 2715 2393 6328 77 - 2 323 11.86 2731 2393 4647 69 - CPU Avg_MHz Busy% Bzy_MHz TSC_MHz IRQ CoreTmp - - 1940 67.46 2884 2393 39920 83 - 0 1827 63.70 2877 2393 21184 83 - """ - cpustats = {} - read_data = '' - with open(self.turbostat_log_file) as f: - read_data = f.readlines() - - if not read_data: - self._logger.LogOutput('WARNING: Turbostat output file is empty.') - return {} - - # First line always contains the header. - stats = read_data[0].split() - - # Mandatory parameters. - if 'CPU' not in stats: - self._logger.LogOutput( - 'WARNING: Missing data for CPU# in Turbostat output.') - return {} - if 'Bzy_MHz' not in stats: - self._logger.LogOutput( - 'WARNING: Missing data for Bzy_MHz in Turbostat output.') - return {} - cpu_index = stats.index('CPU') - cpufreq_index = stats.index('Bzy_MHz') - cpufreq = cpustats.setdefault('cpufreq', {'all': []}) - - # Optional parameters. - cputemp_index = -1 - if 'CoreTmp' in stats: - cputemp_index = stats.index('CoreTmp') - cputemp = cpustats.setdefault('cputemp', {'all': []}) - - # Parse data starting from the second line ignoring repeating headers. - for st in read_data[1:]: - # Data represented by int or float separated by spaces. - numbers = st.split() - if not all(word.replace('.', '', 1).isdigit() for word in numbers[1:]): - # Skip the line if data mismatch. - continue - if numbers[cpu_index] != '-': - # Ignore Core-specific statistics which starts with Core number. - # Combined statistics for all core has "-" CPU identifier. - continue - - cpufreq['all'].append(int(numbers[cpufreq_index])) - if cputemp_index != -1: - cputemp['all'].append(int(numbers[cputemp_index])) - return cpustats - - def ProcessTopResults(self): - """Given self.top_log_file process top log data. - - Returns: - List of dictionaries with the following keyvals: - 'cmd': command name (string), - 'cpu_use_avg': average cpu usage (float), - 'count': number of occurrences (int), - 'top5_cpu_use': up to 5 highest cpu usages (descending list of floats) - - Example of the top log: - PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND - 4102 chronos 12 -8 3454472 238300 118188 R 41.8 6.1 0:08.37 chrome - 375 root 0 -20 0 0 0 S 5.9 0.0 0:00.17 kworker - 617 syslog 20 0 25332 8372 7888 S 5.9 0.2 0:00.77 systemd - - PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND - 5745 chronos 20 0 5438580 139328 67988 R 122.8 3.6 0:04.26 chrome - 912 root -51 0 0 0 0 S 2.0 0.0 0:01.04 irq/cro - 121 root 20 0 0 0 0 S 1.0 0.0 0:00.45 spi5 + This class manages what exactly is stored inside the cache without knowing + what the key of the cache is. For runs with perf, it stores perf.data, + perf.report, etc. The key generation is handled by the ResultsCache class. """ - all_data = '' - with open(self.top_log_file) as f: - all_data = f.read() - if not all_data: - self._logger.LogOutput('WARNING: Top log file is empty.') - return [] - - top_line_regex = re.compile( - r""" + def __init__(self, logger, label, log_level, machine, cmd_exec=None): + self.chromeos_root = label.chromeos_root + self._logger = logger + self.ce = cmd_exec or command_executer.GetCommandExecuter( + self._logger, log_level=log_level + ) + self.temp_dir = None + self.label = label + self.results_dir = None + self.log_level = log_level + self.machine = machine + self.perf_data_files = [] + self.perf_report_files = [] + self.results_file = [] + self.turbostat_log_file = "" + self.cpustats_log_file = "" + self.cpuinfo_file = "" + self.top_log_file = "" + self.wait_time_log_file = "" + self.chrome_version = "" + self.err = None + self.chroot_results_dir = "" + self.test_name = "" + self.keyvals = None + self.board = None + self.suite = None + self.cwp_dso = "" + self.retval = None + self.out = None + self.top_cmds = [] + + def GetTopCmds(self): + """Get the list of top commands consuming CPU on the machine.""" + return self.top_cmds + + def FormatStringTopCommands(self): + """Get formatted string of top commands. + + Get the formatted string with top commands consuming CPU on DUT machine. + Number of "non-chrome" processes in the list is limited to 5. + """ + format_list = [ + "Top commands with highest CPU usage:", + # Header. + "%20s %9s %6s %s" % ("COMMAND", "AVG CPU%", "COUNT", "HIGHEST 5"), + "-" * 50, + ] + if self.top_cmds: + # After switching to top processes we have to expand the list since there + # will be a lot of 'chrome' processes (up to 10, sometimes more) in the + # top. + # Let's limit the list size by the number of non-chrome processes. + limit_of_non_chrome_procs = 5 + num_of_non_chrome_procs = 0 + for topcmd in self.top_cmds: + print_line = "%20s %9.2f %6s %s" % ( + topcmd["cmd"], + topcmd["cpu_use_avg"], + topcmd["count"], + topcmd["top5_cpu_use"], + ) + format_list.append(print_line) + if not topcmd["cmd"].startswith("chrome"): + num_of_non_chrome_procs += 1 + if num_of_non_chrome_procs >= limit_of_non_chrome_procs: + break + else: + format_list.append("[NO DATA FROM THE TOP LOG]") + format_list.append("-" * 50) + return "\n".join(format_list) + + def CopyFilesTo(self, dest_dir, files_to_copy): + file_index = 0 + for file_to_copy in files_to_copy: + if not os.path.isdir(dest_dir): + command = "mkdir -p %s" % dest_dir + self.ce.RunCommand(command) + dest_file = os.path.join( + dest_dir, + ("%s.%s" % (os.path.basename(file_to_copy), file_index)), + ) + ret = self.ce.CopyFiles(file_to_copy, dest_file, recursive=False) + if ret: + raise IOError("Could not copy results file: %s" % file_to_copy) + file_index += 1 + + def CopyResultsTo(self, dest_dir): + self.CopyFilesTo(dest_dir, self.results_file) + self.CopyFilesTo(dest_dir, self.perf_data_files) + self.CopyFilesTo(dest_dir, self.perf_report_files) + extra_files = [] + if self.top_log_file: + extra_files.append(self.top_log_file) + if self.cpuinfo_file: + extra_files.append(self.cpuinfo_file) + if extra_files: + self.CopyFilesTo(dest_dir, extra_files) + if self.results_file or self.perf_data_files or self.perf_report_files: + self._logger.LogOutput("Results files stored in %s." % dest_dir) + + def CompressResultsTo(self, dest_dir): + tarball = os.path.join(self.results_dir, RESULTS_TARBALL) + # Test_that runs hold all output under TEST_NAME_HASHTAG/results/, + # while tast runs hold output under TEST_NAME/. + # Both ensure to be unique. + result_dir_name = self.test_name if self.suite == "tast" else "results" + results_dir = self.FindFilesInResultsDir( + "-name %s" % result_dir_name + ).split("\n")[0] + + if not results_dir: + self._logger.LogOutput( + "WARNING: No results dir matching %r found" % result_dir_name + ) + return + + self.CreateTarball(results_dir, tarball) + self.CopyFilesTo(dest_dir, [tarball]) + if results_dir: + self._logger.LogOutput( + "Results files compressed into %s." % dest_dir + ) + + def GetNewKeyvals(self, keyvals_dict): + # Initialize 'units' dictionary. + units_dict = {} + for k in keyvals_dict: + units_dict[k] = "" + results_files = self.GetDataMeasurementsFiles() + for f in results_files: + # Make sure we can find the results file + if os.path.exists(f): + data_filename = f + else: + # Otherwise get the base filename and create the correct + # path for it. + _, f_base = misc.GetRoot(f) + data_filename = os.path.join( + self.chromeos_root, "chroot/tmp", self.temp_dir, f_base + ) + if data_filename.find(".json") > 0: + raw_dict = dict() + if os.path.exists(data_filename): + with open(data_filename, "r") as data_file: + raw_dict = json.load(data_file) + + if "charts" in raw_dict: + raw_dict = raw_dict["charts"] + for k1 in raw_dict: + field_dict = raw_dict[k1] + for k2 in field_dict: + result_dict = field_dict[k2] + key = k1 + "__" + k2 + if "value" in result_dict: + keyvals_dict[key] = result_dict["value"] + elif "values" in result_dict: + values = result_dict["values"] + if ( + "type" in result_dict + and result_dict["type"] + == "list_of_scalar_values" + and values + and values != "null" + ): + keyvals_dict[key] = sum(values) / float( + len(values) + ) + else: + keyvals_dict[key] = values + units_dict[key] = result_dict["units"] + else: + if os.path.exists(data_filename): + with open(data_filename, "r") as data_file: + lines = data_file.readlines() + for line in lines: + tmp_dict = json.loads(line) + graph_name = tmp_dict["graph"] + graph_str = ( + (graph_name + "__") if graph_name else "" + ) + key = graph_str + tmp_dict["description"] + keyvals_dict[key] = tmp_dict["value"] + units_dict[key] = tmp_dict["units"] + + return keyvals_dict, units_dict + + def AppendTelemetryUnits(self, keyvals_dict, units_dict): + """keyvals_dict is the dict of key-value used to generate Crosperf reports. + + units_dict is a dictionary of the units for the return values in + keyvals_dict. We need to associate the units with the return values, + for Telemetry tests, so that we can include the units in the reports. + This function takes each value in keyvals_dict, finds the corresponding + unit in the units_dict, and replaces the old value with a list of the + old value and the units. This later gets properly parsed in the + ResultOrganizer class, for generating the reports. + """ + + results_dict = {} + for k in keyvals_dict: + # We don't want these lines in our reports; they add no useful data. + if not k or k == "telemetry_Crosperf": + continue + val = keyvals_dict[k] + units = units_dict[k] + new_val = [val, units] + results_dict[k] = new_val + return results_dict + + def GetKeyvals(self): + results_in_chroot = os.path.join(self.chromeos_root, "chroot", "tmp") + if not self.temp_dir: + self.temp_dir = tempfile.mkdtemp(dir=results_in_chroot) + command = f"cp -r {self.results_dir}/* {self.temp_dir}" + self.ce.RunCommand(command, print_to_console=False) + + command = "./generate_test_report --no-color --csv %s" % ( + os.path.join("/tmp", os.path.basename(self.temp_dir)) + ) + _, out, _ = self.ce.ChrootRunCommandWOutput( + self.chromeos_root, command, print_to_console=False + ) + keyvals_dict = {} + tmp_dir_in_chroot = misc.GetInsideChrootPath( + self.chromeos_root, self.temp_dir + ) + for line in out.splitlines(): + tokens = re.split("=|,", line) + key = tokens[-2] + if key.startswith(tmp_dir_in_chroot): + key = key[len(tmp_dir_in_chroot) + 1 :] + value = tokens[-1] + keyvals_dict[key] = value + + # Check to see if there is a perf_measurements file and get the + # data from it if so. + keyvals_dict, units_dict = self.GetNewKeyvals(keyvals_dict) + if self.suite == "telemetry_Crosperf": + # For telemtry_Crosperf results, append the units to the return + # results, for use in generating the reports. + keyvals_dict = self.AppendTelemetryUnits(keyvals_dict, units_dict) + return keyvals_dict + + def GetSamples(self): + actual_samples = 0 + for perf_data_file in self.perf_data_files: + chroot_perf_data_file = misc.GetInsideChrootPath( + self.chromeos_root, perf_data_file + ) + perf_path = os.path.join( + self.chromeos_root, "chroot", "usr/bin/perf" + ) + perf_file = "/usr/sbin/perf" + if os.path.exists(perf_path): + perf_file = "/usr/bin/perf" + + # For each perf.data, we want to collect sample count for specific DSO. + # We specify exact match for known DSO type, and every sample for `all`. + exact_match = "" + if self.cwp_dso == "all": + exact_match = '""' + elif self.cwp_dso == "chrome": + exact_match = '" chrome "' + elif self.cwp_dso == "kallsyms": + exact_match = '"[kernel.kallsyms]"' + else: + # This will need to be updated once there are more DSO types supported, + # if user want an exact match for the field they want. + exact_match = '"%s"' % self.cwp_dso + + command = "%s report -n -s dso -i %s 2> /dev/null | grep %s" % ( + perf_file, + chroot_perf_data_file, + exact_match, + ) + _, result, _ = self.ce.ChrootRunCommandWOutput( + self.chromeos_root, command + ) + # Accumulate the sample count for all matched fields. + # Each line looks like this: + # 45.42% 237210 chrome + # And we want the second number which is the sample count. + samples = 0 + try: + for line in result.split("\n"): + attr = line.split() + if len(attr) == 3 and "%" in attr[0]: + samples += int(attr[1]) + except: + raise RuntimeError("Cannot parse perf dso result") + + actual_samples += samples + + # Remove idle cycles from the accumulated sample count. + perf_report_file = f"{perf_data_file}.report" + if not os.path.exists(perf_report_file): + raise RuntimeError( + f"Missing perf report file: {perf_report_file}" + ) + + idle_functions = { + "[kernel.kallsyms]": ( + "intel_idle", + "arch_cpu_idle", + "intel_idle", + "cpu_startup_entry", + "default_idle", + "cpu_idle_loop", + "do_idle", + ), + } + idle_samples = 0 + + with open(perf_report_file) as f: + try: + for line in f: + line = line.strip() + if not line or line[0] == "#": + continue + # Each line has the following fields, + # pylint: disable=line-too-long + # Overhead Samples Command Shared Object Symbol + # pylint: disable=line-too-long + # 1.48% 60 swapper [kernel.kallsyms] [k] intel_idle + # pylint: disable=line-too-long + # 0.00% 1 shill libshill-net.so [.] std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >::vector<unsigned char const*> + _, samples, _, dso, _, function = line.split(None, 5) + + if ( + dso in idle_functions + and function in idle_functions[dso] + ): + if self.log_level != "verbose": + self._logger.LogOutput( + "Removing %s samples from %s in %s" + % (samples, function, dso) + ) + idle_samples += int(samples) + except: + raise RuntimeError("Cannot parse perf report") + actual_samples -= idle_samples + return [actual_samples, "samples"] + + def GetResultsDir(self): + if self.suite == "tast": + mo = re.search(r"Writing results to (\S+)", self.out) + else: + mo = re.search(r"Results placed in (\S+)", self.out) + if mo: + result = mo.group(1) + return result + raise RuntimeError("Could not find results directory.") + + def FindFilesInResultsDir(self, find_args): + if not self.results_dir: + return "" + + command = "find %s %s" % (self.results_dir, find_args) + ret, out, _ = self.ce.RunCommandWOutput(command, print_to_console=False) + if ret: + raise RuntimeError("Could not run find command!") + return out + + def GetResultsFile(self): + if self.suite == "telemetry_Crosperf": + return self.FindFilesInResultsDir( + "-name histograms.json" + ).splitlines() + return self.FindFilesInResultsDir( + "-name results-chart.json" + ).splitlines() + + def GetPerfDataFiles(self): + return self.FindFilesInResultsDir("-name perf.data").splitlines() + + def GetPerfReportFiles(self): + return self.FindFilesInResultsDir("-name perf.data.report").splitlines() + + def GetDataMeasurementsFiles(self): + result = self.FindFilesInResultsDir( + "-name perf_measurements" + ).splitlines() + if not result: + if self.suite == "telemetry_Crosperf": + result = self.FindFilesInResultsDir( + "-name histograms.json" + ).splitlines() + else: + result = self.FindFilesInResultsDir( + "-name results-chart.json" + ).splitlines() + return result + + def GetTurbostatFile(self): + """Get turbostat log path string.""" + return self.FindFilesInResultsDir("-name turbostat.log").split("\n")[0] + + def GetCpustatsFile(self): + """Get cpustats log path string.""" + return self.FindFilesInResultsDir("-name cpustats.log").split("\n")[0] + + def GetCpuinfoFile(self): + """Get cpustats log path string.""" + return self.FindFilesInResultsDir("-name cpuinfo.log").split("\n")[0] + + def GetTopFile(self): + """Get cpustats log path string.""" + return self.FindFilesInResultsDir("-name top.log").split("\n")[0] + + def GetWaitTimeFile(self): + """Get wait time log path string.""" + return self.FindFilesInResultsDir("-name wait_time.log").split("\n")[0] + + def _CheckDebugPath(self, option, path): + relative_path = path[1:] + out_chroot_path = os.path.join( + self.chromeos_root, "chroot", relative_path + ) + if os.path.exists(out_chroot_path): + if option == "kallsyms": + path = os.path.join(path, "System.map-*") + return "--" + option + " " + path + else: + print( + "** WARNING **: --%s option not applied, %s does not exist" + % (option, out_chroot_path) + ) + return "" + + def GeneratePerfReportFiles(self): + perf_report_files = [] + for perf_data_file in self.perf_data_files: + # Generate a perf.report and store it side-by-side with the perf.data + # file. + chroot_perf_data_file = misc.GetInsideChrootPath( + self.chromeos_root, perf_data_file + ) + perf_report_file = "%s.report" % perf_data_file + if os.path.exists(perf_report_file): + raise RuntimeError( + "Perf report file already exists: %s" % perf_report_file + ) + chroot_perf_report_file = misc.GetInsideChrootPath( + self.chromeos_root, perf_report_file + ) + perf_path = os.path.join( + self.chromeos_root, "chroot", "usr/bin/perf" + ) + + perf_file = "/usr/sbin/perf" + if os.path.exists(perf_path): + perf_file = "/usr/bin/perf" + + debug_path = self.label.debug_path + + if debug_path: + symfs = "--symfs " + debug_path + vmlinux = "--vmlinux " + os.path.join( + debug_path, "usr", "lib", "debug", "boot", "vmlinux" + ) + kallsyms = "" + print( + "** WARNING **: --kallsyms option not applied, no System.map-* " + "for downloaded image." + ) + else: + if self.label.image_type != "local": + print( + "** WARNING **: Using local debug info in /build, this may " + "not match the downloaded image." + ) + build_path = os.path.join("/build", self.board) + symfs = self._CheckDebugPath("symfs", build_path) + vmlinux_path = os.path.join( + build_path, "usr/lib/debug/boot/vmlinux" + ) + vmlinux = self._CheckDebugPath("vmlinux", vmlinux_path) + kallsyms_path = os.path.join(build_path, "boot") + kallsyms = self._CheckDebugPath("kallsyms", kallsyms_path) + + command = "%s report -n %s %s %s -i %s --stdio > %s" % ( + perf_file, + symfs, + vmlinux, + kallsyms, + chroot_perf_data_file, + chroot_perf_report_file, + ) + if self.log_level != "verbose": + self._logger.LogOutput( + "Generating perf report...\nCMD: %s" % command + ) + exit_code = self.ce.ChrootRunCommand(self.chromeos_root, command) + if exit_code == 0: + if self.log_level != "verbose": + self._logger.LogOutput( + "Perf report generated successfully." + ) + else: + raise RuntimeError( + "Perf report not generated correctly. CMD: %s" % command + ) + + # Add a keyval to the dictionary for the events captured. + perf_report_files.append( + misc.GetOutsideChrootPath( + self.chromeos_root, chroot_perf_report_file + ) + ) + return perf_report_files + + def GatherPerfResults(self): + report_id = 0 + for perf_report_file in self.perf_report_files: + with open(perf_report_file, "r") as f: + report_contents = f.read() + for group in re.findall( + r"Events: (\S+) (\S+)", report_contents + ): + num_events = group[0] + event_name = group[1] + key = "perf_%s_%s" % (report_id, event_name) + value = str(misc.UnitToNumber(num_events)) + self.keyvals[key] = value + + def PopulateFromRun(self, out, err, retval, test, suite, cwp_dso): + self.board = self.label.board + self.out = out + self.err = err + self.retval = retval + self.test_name = test + self.suite = suite + self.cwp_dso = cwp_dso + self.chroot_results_dir = self.GetResultsDir() + self.results_dir = misc.GetOutsideChrootPath( + self.chromeos_root, self.chroot_results_dir + ) + self.results_file = self.GetResultsFile() + self.perf_data_files = self.GetPerfDataFiles() + # Include all perf.report data in table. + self.perf_report_files = self.GeneratePerfReportFiles() + self.turbostat_log_file = self.GetTurbostatFile() + self.cpustats_log_file = self.GetCpustatsFile() + self.cpuinfo_file = self.GetCpuinfoFile() + self.top_log_file = self.GetTopFile() + self.wait_time_log_file = self.GetWaitTimeFile() + # TODO(asharif): Do something similar with perf stat. + + # Grab keyvals from the directory. + self.ProcessResults() + + def ProcessChartResults(self): + # Open and parse the json results file generated by telemetry/test_that. + if not self.results_file: + raise IOError("No results file found.") + filename = self.results_file[0] + if not filename.endswith(".json"): + raise IOError( + "Attempt to call json on non-json file: %s" % filename + ) + if not os.path.exists(filename): + raise IOError("%s does not exist" % filename) + + keyvals = {} + with open(filename, "r") as f: + raw_dict = json.load(f) + if "charts" in raw_dict: + raw_dict = raw_dict["charts"] + for k, field_dict in raw_dict.items(): + for item in field_dict: + keyname = k + "__" + item + value_dict = field_dict[item] + if "value" in value_dict: + result = value_dict["value"] + elif "values" in value_dict: + values = value_dict["values"] + if not values: + continue + if ( + "type" in value_dict + and value_dict["type"] == "list_of_scalar_values" + and values != "null" + ): + result = sum(values) / float(len(values)) + else: + result = values + else: + continue + units = value_dict["units"] + new_value = [result, units] + keyvals[keyname] = new_value + return keyvals + + def ProcessTurbostatResults(self): + """Given turbostat_log_file non-null parse cpu stats from file. + + Returns: + Dictionary of 'cpufreq', 'cputemp' where each + includes dictionary 'all': [list_of_values] + + Example of the output of turbostat_log. + ---------------------- + CPU Avg_MHz Busy% Bzy_MHz TSC_MHz IRQ CoreTmp + - 329 12.13 2723 2393 10975 77 + 0 336 12.41 2715 2393 6328 77 + 2 323 11.86 2731 2393 4647 69 + CPU Avg_MHz Busy% Bzy_MHz TSC_MHz IRQ CoreTmp + - 1940 67.46 2884 2393 39920 83 + 0 1827 63.70 2877 2393 21184 83 + """ + cpustats = {} + read_data = "" + with open(self.turbostat_log_file) as f: + read_data = f.readlines() + + if not read_data: + self._logger.LogOutput("WARNING: Turbostat output file is empty.") + return {} + + # First line always contains the header. + stats = read_data[0].split() + + # Mandatory parameters. + if "CPU" not in stats: + self._logger.LogOutput( + "WARNING: Missing data for CPU# in Turbostat output." + ) + return {} + if "Bzy_MHz" not in stats: + self._logger.LogOutput( + "WARNING: Missing data for Bzy_MHz in Turbostat output." + ) + return {} + cpu_index = stats.index("CPU") + cpufreq_index = stats.index("Bzy_MHz") + cpufreq = cpustats.setdefault("cpufreq", {"all": []}) + + # Optional parameters. + cputemp_index = -1 + if "CoreTmp" in stats: + cputemp_index = stats.index("CoreTmp") + cputemp = cpustats.setdefault("cputemp", {"all": []}) + + # Parse data starting from the second line ignoring repeating headers. + for st in read_data[1:]: + # Data represented by int or float separated by spaces. + numbers = st.split() + if not all( + word.replace(".", "", 1).isdigit() for word in numbers[1:] + ): + # Skip the line if data mismatch. + continue + if numbers[cpu_index] != "-": + # Ignore Core-specific statistics which starts with Core number. + # Combined statistics for all core has "-" CPU identifier. + continue + + cpufreq["all"].append(int(numbers[cpufreq_index])) + if cputemp_index != -1: + cputemp["all"].append(int(numbers[cputemp_index])) + return cpustats + + def ProcessTopResults(self): + """Given self.top_log_file process top log data. + + Returns: + List of dictionaries with the following keyvals: + 'cmd': command name (string), + 'cpu_use_avg': average cpu usage (float), + 'count': number of occurrences (int), + 'top5_cpu_use': up to 5 highest cpu usages (descending list of floats) + + Example of the top log: + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 4102 chronos 12 -8 3454472 238300 118188 R 41.8 6.1 0:08.37 chrome + 375 root 0 -20 0 0 0 S 5.9 0.0 0:00.17 kworker + 617 syslog 20 0 25332 8372 7888 S 5.9 0.2 0:00.77 systemd + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 5745 chronos 20 0 5438580 139328 67988 R 122.8 3.6 0:04.26 chrome + 912 root -51 0 0 0 0 S 2.0 0.0 0:01.04 irq/cro + 121 root 20 0 0 0 0 S 1.0 0.0 0:00.45 spi5 + """ + all_data = "" + with open(self.top_log_file) as f: + all_data = f.read() + + if not all_data: + self._logger.LogOutput("WARNING: Top log file is empty.") + return [] + + top_line_regex = re.compile( + r""" ^\s*(?P<pid>\d+)\s+ # Group 1: PID \S+\s+\S+\s+-?\d+\s+ # Ignore: user, prio, nice \d+\s+\d+\s+\d+\s+ # Ignore: virt/res/shared mem @@ -663,814 +748,922 @@ class Result(object): (?P<cpu_use>\d+\.\d+)\s+ # Group 2: CPU usage \d+\.\d+\s+\d+:\d+\.\d+\s+ # Ignore: mem usage, time (?P<cmd>\S+)$ # Group 3: command - """, re.VERBOSE) - # Page represents top log data per one measurement within time interval - # 'top_interval'. - # Pages separated by empty line. - pages = all_data.split('\n\n') - # Snapshots are structured representation of the pages. - snapshots = [] - for page in pages: - if not page: - continue - - # Snapshot list will contain all processes (command duplicates are - # allowed). - snapshot = [] - for line in page.splitlines(): - match = top_line_regex.match(line) - if match: - # Top line is valid, collect data. - process = { - # NOTE: One command may be represented by multiple processes. - 'cmd': match.group('cmd'), - 'pid': match.group('pid'), - 'cpu_use': float(match.group('cpu_use')), - } - - # Filter out processes with 0 CPU usage and top command. - if process['cpu_use'] > 0 and process['cmd'] != 'top': - snapshot.append(process) - - # If page contained meaningful data add snapshot to the list. - if snapshot: - snapshots.append(snapshot) - - # Define threshold of CPU usage when Chrome is busy, i.e. benchmark is - # running. - # Ideally it should be 100% but it will be hardly reachable with 1 core. - # Statistics on DUT with 2-6 cores shows that chrome load of 100%, 95% and - # 90% equally occurs in 72-74% of all top log snapshots. - # Further decreasing of load threshold leads to a shifting percent of - # "high load" snapshots which might include snapshots when benchmark is - # not running. - # On 1-core DUT 90% chrome cpu load occurs in 55%, 95% in 33% and 100% in 2% - # of snapshots accordingly. - # Threshold of "high load" is reduced to 70% (from 90) when we switched to - # topstats per process. From experiment data the rest 20% are distributed - # among other chrome processes. - CHROME_HIGH_CPU_LOAD = 70 - # Number of snapshots where chrome is heavily used. - high_load_snapshots = 0 - # Total CPU use per process in ALL active snapshots. - cmd_total_cpu_use = collections.defaultdict(float) - # Top CPU usages per command. - cmd_top5_cpu_use = collections.defaultdict(list) - # List of Top Commands to be returned. - topcmds = [] - - for snapshot_processes in snapshots: - # CPU usage per command, per PID in one snapshot. - cmd_cpu_use_per_snapshot = collections.defaultdict(dict) - for process in snapshot_processes: - cmd = process['cmd'] - cpu_use = process['cpu_use'] - pid = process['pid'] - cmd_cpu_use_per_snapshot[cmd][pid] = cpu_use - - # Chrome processes, pid: cpu_usage. - chrome_processes = cmd_cpu_use_per_snapshot.get('chrome', {}) - chrome_cpu_use_list = chrome_processes.values() - - if chrome_cpu_use_list and max( - chrome_cpu_use_list) > CHROME_HIGH_CPU_LOAD: - # CPU usage of any of the "chrome" processes exceeds "High load" - # threshold which means DUT is busy running a benchmark. - high_load_snapshots += 1 - for cmd, cpu_use_per_pid in cmd_cpu_use_per_snapshot.items(): - for pid, cpu_use in cpu_use_per_pid.items(): - # Append PID to the name of the command. - cmd_with_pid = cmd + '-' + pid - cmd_total_cpu_use[cmd_with_pid] += cpu_use - - # Add cpu_use into command top cpu usages, sorted in descending - # order. - heapq.heappush(cmd_top5_cpu_use[cmd_with_pid], round(cpu_use, 1)) - - for consumer, usage in sorted(cmd_total_cpu_use.items(), - key=lambda x: x[1], - reverse=True): - # Iterate through commands by descending order of total CPU usage. - topcmd = { - 'cmd': consumer, - 'cpu_use_avg': usage / high_load_snapshots, - 'count': len(cmd_top5_cpu_use[consumer]), - 'top5_cpu_use': heapq.nlargest(5, cmd_top5_cpu_use[consumer]), - } - topcmds.append(topcmd) - - return topcmds - - def ProcessCpustatsResults(self): - """Given cpustats_log_file non-null parse cpu data from file. - - Returns: - Dictionary of 'cpufreq', 'cputemp' where each - includes dictionary of parameter: [list_of_values] - - Example of cpustats.log output. - ---------------------- - /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq 1512000 - /sys/devices/system/cpu/cpu2/cpufreq/cpuinfo_cur_freq 2016000 - little-cpu 41234 - big-cpu 51234 - - If cores share the same policy their frequencies may always match - on some devices. - To make report concise we should eliminate redundancy in the output. - Function removes cpuN data if it duplicates data from other cores. - """ - - cpustats = {} - read_data = '' - with open(self.cpustats_log_file) as f: - read_data = f.readlines() - - if not read_data: - self._logger.LogOutput('WARNING: Cpustats output file is empty.') - return {} - - cpufreq_regex = re.compile(r'^[/\S]+/(cpu\d+)/[/\S]+\s+(\d+)$') - cputemp_regex = re.compile(r'^([^/\s]+)\s+(\d+)$') - - for st in read_data: - match = cpufreq_regex.match(st) - if match: - cpu = match.group(1) - # CPU frequency comes in kHz. - freq_khz = int(match.group(2)) - freq_mhz = freq_khz / 1000 - # cpufreq represents a dictionary with CPU frequency-related - # data from cpustats.log. - cpufreq = cpustats.setdefault('cpufreq', {}) - cpu_n_freq = cpufreq.setdefault(cpu, []) - cpu_n_freq.append(freq_mhz) - else: - match = cputemp_regex.match(st) - if match: - therm_type = match.group(1) - # The value is int, uCelsius unit. - temp_uc = float(match.group(2)) - # Round to XX.X float. - temp_c = round(temp_uc / 1000, 1) - # cputemp represents a dictionary with temperature measurements - # from cpustats.log. - cputemp = cpustats.setdefault('cputemp', {}) - therm_type = cputemp.setdefault(therm_type, []) - therm_type.append(temp_c) - - # Remove duplicate statistics from cpustats. - pruned_stats = {} - for cpukey, cpuparam in cpustats.items(): - # Copy 'cpufreq' and 'cputemp'. - pruned_params = pruned_stats.setdefault(cpukey, {}) - for paramkey, paramvalue in sorted(cpuparam.items()): - # paramvalue is list of all measured data. - if paramvalue not in pruned_params.values(): - pruned_params[paramkey] = paramvalue - - return pruned_stats - - def ProcessHistogramsResults(self): - # Open and parse the json results file generated by telemetry/test_that. - if not self.results_file: - raise IOError('No results file found.') - filename = self.results_file[0] - if not filename.endswith('.json'): - raise IOError('Attempt to call json on non-json file: %s' % filename) - if not os.path.exists(filename): - raise IOError('%s does not exist' % filename) - - keyvals = {} - with open(filename) as f: - histograms = json.load(f) - value_map = {} - # Gets generic set values. - for obj in histograms: - if 'type' in obj and obj['type'] == 'GenericSet': - value_map[obj['guid']] = obj['values'] - - for obj in histograms: - if 'name' not in obj or 'sampleValues' not in obj: - continue - metric_name = obj['name'] - vals = obj['sampleValues'] - if isinstance(vals, list): - # Remove None elements from the list - vals = [val for val in vals if val is not None] - if vals: + """, + re.VERBOSE, + ) + # Page represents top log data per one measurement within time interval + # 'top_interval'. + # Pages separated by empty line. + pages = all_data.split("\n\n") + # Snapshots are structured representation of the pages. + snapshots = [] + for page in pages: + if not page: + continue + + # Snapshot list will contain all processes (command duplicates are + # allowed). + snapshot = [] + for line in page.splitlines(): + match = top_line_regex.match(line) + if match: + # Top line is valid, collect data. + process = { + # NOTE: One command may be represented by multiple processes. + "cmd": match.group("cmd"), + "pid": match.group("pid"), + "cpu_use": float(match.group("cpu_use")), + } + + # Filter out processes with 0 CPU usage and top command. + if process["cpu_use"] > 0 and process["cmd"] != "top": + snapshot.append(process) + + # If page contained meaningful data add snapshot to the list. + if snapshot: + snapshots.append(snapshot) + + # Define threshold of CPU usage when Chrome is busy, i.e. benchmark is + # running. + # Ideally it should be 100% but it will be hardly reachable with 1 core. + # Statistics on DUT with 2-6 cores shows that chrome load of 100%, 95% and + # 90% equally occurs in 72-74% of all top log snapshots. + # Further decreasing of load threshold leads to a shifting percent of + # "high load" snapshots which might include snapshots when benchmark is + # not running. + # On 1-core DUT 90% chrome cpu load occurs in 55%, 95% in 33% and 100% in 2% + # of snapshots accordingly. + # Threshold of "high load" is reduced to 70% (from 90) when we switched to + # topstats per process. From experiment data the rest 20% are distributed + # among other chrome processes. + CHROME_HIGH_CPU_LOAD = 70 + # Number of snapshots where chrome is heavily used. + high_load_snapshots = 0 + # Total CPU use per process in ALL active snapshots. + cmd_total_cpu_use = collections.defaultdict(float) + # Top CPU usages per command. + cmd_top5_cpu_use = collections.defaultdict(list) + # List of Top Commands to be returned. + topcmds = [] + + for snapshot_processes in snapshots: + # CPU usage per command, per PID in one snapshot. + cmd_cpu_use_per_snapshot = collections.defaultdict(dict) + for process in snapshot_processes: + cmd = process["cmd"] + cpu_use = process["cpu_use"] + pid = process["pid"] + cmd_cpu_use_per_snapshot[cmd][pid] = cpu_use + + # Chrome processes, pid: cpu_usage. + chrome_processes = cmd_cpu_use_per_snapshot.get("chrome", {}) + chrome_cpu_use_list = chrome_processes.values() + + if ( + chrome_cpu_use_list + and max(chrome_cpu_use_list) > CHROME_HIGH_CPU_LOAD + ): + # CPU usage of any of the "chrome" processes exceeds "High load" + # threshold which means DUT is busy running a benchmark. + high_load_snapshots += 1 + for cmd, cpu_use_per_pid in cmd_cpu_use_per_snapshot.items(): + for pid, cpu_use in cpu_use_per_pid.items(): + # Append PID to the name of the command. + cmd_with_pid = cmd + "-" + pid + cmd_total_cpu_use[cmd_with_pid] += cpu_use + + # Add cpu_use into command top cpu usages, sorted in descending + # order. + heapq.heappush( + cmd_top5_cpu_use[cmd_with_pid], round(cpu_use, 1) + ) + + for consumer, usage in sorted( + cmd_total_cpu_use.items(), key=lambda x: x[1], reverse=True + ): + # Iterate through commands by descending order of total CPU usage. + topcmd = { + "cmd": consumer, + "cpu_use_avg": usage / high_load_snapshots, + "count": len(cmd_top5_cpu_use[consumer]), + "top5_cpu_use": heapq.nlargest(5, cmd_top5_cpu_use[consumer]), + } + topcmds.append(topcmd) + + return topcmds + + def ProcessCpustatsResults(self): + """Given cpustats_log_file non-null parse cpu data from file. + + Returns: + Dictionary of 'cpufreq', 'cputemp' where each + includes dictionary of parameter: [list_of_values] + + Example of cpustats.log output. + ---------------------- + /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq 1512000 + /sys/devices/system/cpu/cpu2/cpufreq/cpuinfo_cur_freq 2016000 + little-cpu 41234 + big-cpu 51234 + + If cores share the same policy their frequencies may always match + on some devices. + To make report concise we should eliminate redundancy in the output. + Function removes cpuN data if it duplicates data from other cores. + """ + + cpustats = {} + read_data = "" + with open(self.cpustats_log_file) as f: + read_data = f.readlines() + + if not read_data: + self._logger.LogOutput("WARNING: Cpustats output file is empty.") + return {} + + cpufreq_regex = re.compile(r"^[/\S]+/(cpu\d+)/[/\S]+\s+(\d+)$") + cputemp_regex = re.compile(r"^([^/\s]+)\s+(\d+)$") + + for st in read_data: + match = cpufreq_regex.match(st) + if match: + cpu = match.group(1) + # CPU frequency comes in kHz. + freq_khz = int(match.group(2)) + freq_mhz = freq_khz / 1000 + # cpufreq represents a dictionary with CPU frequency-related + # data from cpustats.log. + cpufreq = cpustats.setdefault("cpufreq", {}) + cpu_n_freq = cpufreq.setdefault(cpu, []) + cpu_n_freq.append(freq_mhz) + else: + match = cputemp_regex.match(st) + if match: + therm_type = match.group(1) + # The value is int, uCelsius unit. + temp_uc = float(match.group(2)) + # Round to XX.X float. + temp_c = round(temp_uc / 1000, 1) + # cputemp represents a dictionary with temperature measurements + # from cpustats.log. + cputemp = cpustats.setdefault("cputemp", {}) + therm_type = cputemp.setdefault(therm_type, []) + therm_type.append(temp_c) + + # Remove duplicate statistics from cpustats. + pruned_stats = {} + for cpukey, cpuparam in cpustats.items(): + # Copy 'cpufreq' and 'cputemp'. + pruned_params = pruned_stats.setdefault(cpukey, {}) + for paramkey, paramvalue in sorted(cpuparam.items()): + # paramvalue is list of all measured data. + if paramvalue not in pruned_params.values(): + pruned_params[paramkey] = paramvalue + + return pruned_stats + + def ProcessHistogramsResults(self): + # Open and parse the json results file generated by telemetry/test_that. + if not self.results_file: + raise IOError("No results file found.") + filename = self.results_file[0] + if not filename.endswith(".json"): + raise IOError( + "Attempt to call json on non-json file: %s" % filename + ) + if not os.path.exists(filename): + raise IOError("%s does not exist" % filename) + + keyvals = {} + with open(filename) as f: + histograms = json.load(f) + value_map = {} + # Gets generic set values. + for obj in histograms: + if "type" in obj and obj["type"] == "GenericSet": + value_map[obj["guid"]] = obj["values"] + + for obj in histograms: + if "name" not in obj or "sampleValues" not in obj: + continue + metric_name = obj["name"] + vals = obj["sampleValues"] + if isinstance(vals, list): + # Remove None elements from the list + vals = [val for val in vals if val is not None] + if vals: + result = float(sum(vals)) / len(vals) + else: + result = 0 + else: + result = vals + unit = obj["unit"] + diagnostics = obj["diagnostics"] + # for summaries of benchmarks + key = metric_name + if key not in keyvals: + keyvals[key] = [[result], unit] + else: + keyvals[key][0].append(result) + # TODO: do we need summaries of stories? + # for summaries of story tags + if "storyTags" in diagnostics: + guid = diagnostics["storyTags"] + if guid not in value_map: + raise RuntimeError( + "Unrecognized storyTags in %s " % (obj) + ) + for story_tag in value_map[guid]: + key = metric_name + "__" + story_tag + if key not in keyvals: + keyvals[key] = [[result], unit] + else: + keyvals[key][0].append(result) + # calculate summary + for key in keyvals: + vals = keyvals[key][0] + unit = keyvals[key][1] result = float(sum(vals)) / len(vals) - else: - result = 0 - else: - result = vals - unit = obj['unit'] - diagnostics = obj['diagnostics'] - # for summaries of benchmarks - key = metric_name - if key not in keyvals: - keyvals[key] = [[result], unit] + keyvals[key] = [result, unit] + return keyvals + + def ReadPidFromPerfData(self): + """Read PIDs from perf.data files. + + Extract PID from perf.data if "perf record" was running per process, + i.e. with "-p <PID>" and no "-a". + + Returns: + pids: list of PIDs. + + Raises: + PerfDataReadError when perf.data header reading fails. + """ + cmd = ["/usr/bin/perf", "report", "--header-only", "-i"] + pids = [] + + for perf_data_path in self.perf_data_files: + perf_data_path_in_chroot = misc.GetInsideChrootPath( + self.chromeos_root, perf_data_path + ) + path_str = " ".join(cmd + [perf_data_path_in_chroot]) + status, output, _ = self.ce.ChrootRunCommandWOutput( + self.chromeos_root, path_str + ) + if status: + # Error of reading a perf.data profile is fatal. + raise PerfDataReadError( + f"Failed to read perf.data profile: {path_str}" + ) + + # Pattern to search a line with "perf record" command line: + # # cmdline : /usr/bin/perf record -e instructions -p 123" + cmdline_regex = re.compile( + r"^\#\scmdline\s:\s+(?P<cmd>.*perf\s+record\s+.*)$" + ) + # Pattern to search PID in a command line. + pid_regex = re.compile(r"^.*\s-p\s(?P<pid>\d+)\s*.*$") + for line in output.splitlines(): + cmd_match = cmdline_regex.match(line) + if cmd_match: + # Found a perf command line. + cmdline = cmd_match.group("cmd") + # '-a' is a system-wide mode argument. + if "-a" not in cmdline.split(): + # It can be that perf was attached to PID and was still running in + # system-wide mode. + # We filter out this case here since it's not per-process. + pid_match = pid_regex.match(cmdline) + if pid_match: + pids.append(pid_match.group("pid")) + # Stop the search and move to the next perf.data file. + break + else: + # cmdline wasn't found in the header. It's a fatal error. + raise PerfDataReadError( + f"Perf command line is not found in {path_str}" + ) + return pids + + def VerifyPerfDataPID(self): + """Verify PIDs in per-process perf.data profiles. + + Check that at list one top process is profiled if perf was running in + per-process mode. + + Raises: + PidVerificationError if PID verification of per-process perf.data profiles + fail. + """ + perf_data_pids = self.ReadPidFromPerfData() + if not perf_data_pids: + # In system-wide mode there are no PIDs. + self._logger.LogOutput("System-wide perf mode. Skip verification.") + return + + # PIDs will be present only in per-process profiles. + # In this case we need to verify that profiles are collected on the + # hottest processes. + top_processes = [top_cmd["cmd"] for top_cmd in self.top_cmds] + # top_process structure: <cmd>-<pid> + top_pids = [top_process.split("-")[-1] for top_process in top_processes] + for top_pid in top_pids: + if top_pid in perf_data_pids: + self._logger.LogOutput( + "PID verification passed! " + f"Top process {top_pid} is profiled." + ) + return + raise PidVerificationError( + f"top processes {top_processes} are missing in perf.data traces with" + f" PID: {perf_data_pids}." + ) + + def ProcessResults(self, use_cache=False): + # Note that this function doesn't know anything about whether there is a + # cache hit or miss. It should process results agnostic of the cache hit + # state. + if ( + self.results_file + and self.suite == "telemetry_Crosperf" + and "histograms.json" in self.results_file[0] + ): + self.keyvals = self.ProcessHistogramsResults() + elif ( + self.results_file + and self.suite != "telemetry_Crosperf" + and "results-chart.json" in self.results_file[0] + ): + self.keyvals = self.ProcessChartResults() else: - keyvals[key][0].append(result) - # TODO: do we need summaries of stories? - # for summaries of story tags - if 'storyTags' in diagnostics: - guid = diagnostics['storyTags'] - if guid not in value_map: - raise RuntimeError('Unrecognized storyTags in %s ' % (obj)) - for story_tag in value_map[guid]: - key = metric_name + '__' + story_tag - if key not in keyvals: - keyvals[key] = [[result], unit] + if not use_cache: + print( + "\n ** WARNING **: Had to use deprecated output-method to " + "collect results.\n" + ) + self.keyvals = self.GetKeyvals() + self.keyvals["retval"] = self.retval + # If we are in CWP approximation mode, we want to collect DSO samples + # for each perf.data file + if self.cwp_dso and self.retval == 0: + self.keyvals["samples"] = self.GetSamples() + # If the samples count collected from perf file is 0, we will treat + # it as a failed run. + if self.keyvals["samples"][0] == 0: + del self.keyvals["samples"] + self.keyvals["retval"] = 1 + # Generate report from all perf.data files. + # Now parse all perf report files and include them in keyvals. + self.GatherPerfResults() + + cpustats = {} + # Turbostat output has higher priority of processing. + if self.turbostat_log_file: + cpustats = self.ProcessTurbostatResults() + # Process cpustats output only if turbostat has no data. + if not cpustats and self.cpustats_log_file: + cpustats = self.ProcessCpustatsResults() + if self.top_log_file: + self.top_cmds = self.ProcessTopResults() + # Verify that PID in non system-wide perf.data and top_cmds are matching. + if self.perf_data_files and self.top_cmds: + self.VerifyPerfDataPID() + if self.wait_time_log_file: + with open(self.wait_time_log_file) as f: + wait_time = f.readline().strip() + try: + wait_time = float(wait_time) + except ValueError: + raise ValueError("Wait time in log file is not a number.") + # This is for accumulating wait time for telemtry_Crosperf runs only, + # for test_that runs, please refer to suite_runner. + self.machine.AddCooldownWaitTime(wait_time) + + for param_key, param in cpustats.items(): + for param_type, param_values in param.items(): + val_avg = sum(param_values) / len(param_values) + val_min = min(param_values) + val_max = max(param_values) + # Average data is always included. + self.keyvals["_".join([param_key, param_type, "avg"])] = val_avg + # Insert min/max results only if they deviate + # from average. + if val_min != val_avg: + self.keyvals[ + "_".join([param_key, param_type, "min"]) + ] = val_min + if val_max != val_avg: + self.keyvals[ + "_".join([param_key, param_type, "max"]) + ] = val_max + + def GetChromeVersionFromCache(self, cache_dir): + # Read chrome_version from keys file, if present. + chrome_version = "" + keys_file = os.path.join(cache_dir, CACHE_KEYS_FILE) + if os.path.exists(keys_file): + with open(keys_file, "r") as f: + lines = f.readlines() + for l in lines: + if l.startswith("Google Chrome "): + chrome_version = l + if chrome_version.endswith("\n"): + chrome_version = chrome_version[:-1] + break + return chrome_version + + def PopulateFromCacheDir(self, cache_dir, test, suite, cwp_dso): + self.test_name = test + self.suite = suite + self.cwp_dso = cwp_dso + # Read in everything from the cache directory. + with open(os.path.join(cache_dir, RESULTS_FILE), "rb") as f: + self.out = pickle.load(f) + self.err = pickle.load(f) + self.retval = pickle.load(f) + + # Untar the tarball to a temporary directory + self.temp_dir = tempfile.mkdtemp( + dir=os.path.join(self.chromeos_root, "chroot", "tmp") + ) + + command = "cd %s && tar xf %s" % ( + self.temp_dir, + os.path.join(cache_dir, AUTOTEST_TARBALL), + ) + ret = self.ce.RunCommand(command, print_to_console=False) + if ret: + raise RuntimeError("Could not untar cached tarball") + self.results_dir = self.temp_dir + self.results_file = self.GetDataMeasurementsFiles() + self.perf_data_files = self.GetPerfDataFiles() + self.perf_report_files = self.GetPerfReportFiles() + self.chrome_version = self.GetChromeVersionFromCache(cache_dir) + self.ProcessResults(use_cache=True) + + def CleanUp(self, rm_chroot_tmp): + if rm_chroot_tmp and self.results_dir: + dirname, basename = misc.GetRoot(self.results_dir) + if basename.find("test_that_results_") != -1: + command = "rm -rf %s" % self.results_dir else: - keyvals[key][0].append(result) - # calculate summary - for key in keyvals: - vals = keyvals[key][0] - unit = keyvals[key][1] - result = float(sum(vals)) / len(vals) - keyvals[key] = [result, unit] - return keyvals - - def ReadPidFromPerfData(self): - """Read PIDs from perf.data files. - - Extract PID from perf.data if "perf record" was running per process, - i.e. with "-p <PID>" and no "-a". - - Returns: - pids: list of PIDs. - - Raises: - PerfDataReadError when perf.data header reading fails. - """ - cmd = ['/usr/bin/perf', 'report', '--header-only', '-i'] - pids = [] - - for perf_data_path in self.perf_data_files: - perf_data_path_in_chroot = misc.GetInsideChrootPath( - self.chromeos_root, perf_data_path) - path_str = ' '.join(cmd + [perf_data_path_in_chroot]) - status, output, _ = self.ce.ChrootRunCommandWOutput( - self.chromeos_root, path_str) - if status: - # Error of reading a perf.data profile is fatal. - raise PerfDataReadError( - f'Failed to read perf.data profile: {path_str}') - - # Pattern to search a line with "perf record" command line: - # # cmdline : /usr/bin/perf record -e instructions -p 123" - cmdline_regex = re.compile( - r'^\#\scmdline\s:\s+(?P<cmd>.*perf\s+record\s+.*)$') - # Pattern to search PID in a command line. - pid_regex = re.compile(r'^.*\s-p\s(?P<pid>\d+)\s*.*$') - for line in output.splitlines(): - cmd_match = cmdline_regex.match(line) - if cmd_match: - # Found a perf command line. - cmdline = cmd_match.group('cmd') - # '-a' is a system-wide mode argument. - if '-a' not in cmdline.split(): - # It can be that perf was attached to PID and was still running in - # system-wide mode. - # We filter out this case here since it's not per-process. - pid_match = pid_regex.match(cmdline) - if pid_match: - pids.append(pid_match.group('pid')) - # Stop the search and move to the next perf.data file. - break - else: - # cmdline wasn't found in the header. It's a fatal error. - raise PerfDataReadError( - f'Perf command line is not found in {path_str}') - return pids - - def VerifyPerfDataPID(self): - """Verify PIDs in per-process perf.data profiles. - - Check that at list one top process is profiled if perf was running in - per-process mode. - - Raises: - PidVerificationError if PID verification of per-process perf.data profiles - fail. - """ - perf_data_pids = self.ReadPidFromPerfData() - if not perf_data_pids: - # In system-wide mode there are no PIDs. - self._logger.LogOutput('System-wide perf mode. Skip verification.') - return - - # PIDs will be present only in per-process profiles. - # In this case we need to verify that profiles are collected on the - # hottest processes. - top_processes = [top_cmd['cmd'] for top_cmd in self.top_cmds] - # top_process structure: <cmd>-<pid> - top_pids = [top_process.split('-')[-1] for top_process in top_processes] - for top_pid in top_pids: - if top_pid in perf_data_pids: - self._logger.LogOutput('PID verification passed! ' - f'Top process {top_pid} is profiled.') - return - raise PidVerificationError( - f'top processes {top_processes} are missing in perf.data traces with' - f' PID: {perf_data_pids}.') - - def ProcessResults(self, use_cache=False): - # Note that this function doesn't know anything about whether there is a - # cache hit or miss. It should process results agnostic of the cache hit - # state. - if (self.results_file and self.suite == 'telemetry_Crosperf' - and 'histograms.json' in self.results_file[0]): - self.keyvals = self.ProcessHistogramsResults() - elif (self.results_file and self.suite != 'telemetry_Crosperf' - and 'results-chart.json' in self.results_file[0]): - self.keyvals = self.ProcessChartResults() - else: - if not use_cache: - print('\n ** WARNING **: Had to use deprecated output-method to ' - 'collect results.\n') - self.keyvals = self.GetKeyvals() - self.keyvals['retval'] = self.retval - # If we are in CWP approximation mode, we want to collect DSO samples - # for each perf.data file - if self.cwp_dso and self.retval == 0: - self.keyvals['samples'] = self.GetSamples() - # If the samples count collected from perf file is 0, we will treat - # it as a failed run. - if self.keyvals['samples'][0] == 0: - del self.keyvals['samples'] - self.keyvals['retval'] = 1 - # Generate report from all perf.data files. - # Now parse all perf report files and include them in keyvals. - self.GatherPerfResults() - - cpustats = {} - # Turbostat output has higher priority of processing. - if self.turbostat_log_file: - cpustats = self.ProcessTurbostatResults() - # Process cpustats output only if turbostat has no data. - if not cpustats and self.cpustats_log_file: - cpustats = self.ProcessCpustatsResults() - if self.top_log_file: - self.top_cmds = self.ProcessTopResults() - # Verify that PID in non system-wide perf.data and top_cmds are matching. - if self.perf_data_files and self.top_cmds: - self.VerifyPerfDataPID() - if self.wait_time_log_file: - with open(self.wait_time_log_file) as f: - wait_time = f.readline().strip() + command = "rm -rf %s" % dirname + self.ce.RunCommand(command) + if self.temp_dir: + command = "rm -rf %s" % self.temp_dir + self.ce.RunCommand(command) + + def CreateTarball(self, results_dir, tarball): + if not results_dir.strip(): + raise ValueError( + "Refusing to `tar` an empty results_dir: %r" % results_dir + ) + + ret = self.ce.RunCommand( + "cd %s && " + "tar " + "--exclude=var/spool " + "--exclude=var/log " + "-cjf %s ." % (results_dir, tarball) + ) + if ret: + raise RuntimeError("Couldn't compress test output directory.") + + def StoreToCacheDir(self, cache_dir, machine_manager, key_list): + # Create the dir if it doesn't exist. + temp_dir = tempfile.mkdtemp() + + # Store to the temp directory. + with open(os.path.join(temp_dir, RESULTS_FILE), "wb") as f: + pickle.dump(self.out, f) + pickle.dump(self.err, f) + pickle.dump(self.retval, f) + + if not test_flag.GetTestMode(): + with open(os.path.join(temp_dir, CACHE_KEYS_FILE), "w") as f: + f.write("%s\n" % self.label.name) + f.write("%s\n" % self.label.chrome_version) + f.write("%s\n" % self.machine.checksum_string) + for k in key_list: + f.write(k) + f.write("\n") + + if self.results_dir: + tarball = os.path.join(temp_dir, AUTOTEST_TARBALL) + self.CreateTarball(self.results_dir, tarball) + + # Store machine info. + # TODO(asharif): Make machine_manager a singleton, and don't pass it into + # this function. + with open(os.path.join(temp_dir, MACHINE_FILE), "w") as f: + f.write(machine_manager.machine_checksum_string[self.label.name]) + + if os.path.exists(cache_dir): + command = f"rm -rf {cache_dir}" + self.ce.RunCommand(command) + + parent_dir = os.path.dirname(cache_dir) + command = f"mkdir -p {parent_dir} && " + command += f"chmod g+x {temp_dir} && " + command += f"mv {temp_dir} {cache_dir}" + ret = self.ce.RunCommand(command) + if ret: + command = f"rm -rf {temp_dir}" + self.ce.RunCommand(command) + raise RuntimeError( + "Could not move dir %s to dir %s" % (temp_dir, cache_dir) + ) + + @classmethod + def CreateFromRun( + cls, + logger, + log_level, + label, + machine, + out, + err, + retval, + test, + suite="telemetry_Crosperf", + cwp_dso="", + ): + if suite == "telemetry": + result = TelemetryResult(logger, label, log_level, machine) + else: + result = cls(logger, label, log_level, machine) + result.PopulateFromRun(out, err, retval, test, suite, cwp_dso) + return result + + @classmethod + def CreateFromCacheHit( + cls, + logger, + log_level, + label, + machine, + cache_dir, + test, + suite="telemetry_Crosperf", + cwp_dso="", + ): + if suite == "telemetry": + result = TelemetryResult(logger, label, log_level, machine) + else: + result = cls(logger, label, log_level, machine) try: - wait_time = float(wait_time) - except ValueError: - raise ValueError('Wait time in log file is not a number.') - # This is for accumulating wait time for telemtry_Crosperf runs only, - # for test_that runs, please refer to suite_runner. - self.machine.AddCooldownWaitTime(wait_time) - - for param_key, param in cpustats.items(): - for param_type, param_values in param.items(): - val_avg = sum(param_values) / len(param_values) - val_min = min(param_values) - val_max = max(param_values) - # Average data is always included. - self.keyvals['_'.join([param_key, param_type, 'avg'])] = val_avg - # Insert min/max results only if they deviate - # from average. - if val_min != val_avg: - self.keyvals['_'.join([param_key, param_type, 'min'])] = val_min - if val_max != val_avg: - self.keyvals['_'.join([param_key, param_type, 'max'])] = val_max - - def GetChromeVersionFromCache(self, cache_dir): - # Read chrome_version from keys file, if present. - chrome_version = '' - keys_file = os.path.join(cache_dir, CACHE_KEYS_FILE) - if os.path.exists(keys_file): - with open(keys_file, 'r') as f: - lines = f.readlines() - for l in lines: - if l.startswith('Google Chrome '): - chrome_version = l - if chrome_version.endswith('\n'): - chrome_version = chrome_version[:-1] - break - return chrome_version - - def PopulateFromCacheDir(self, cache_dir, test, suite, cwp_dso): - self.test_name = test - self.suite = suite - self.cwp_dso = cwp_dso - # Read in everything from the cache directory. - with open(os.path.join(cache_dir, RESULTS_FILE), 'rb') as f: - self.out = pickle.load(f) - self.err = pickle.load(f) - self.retval = pickle.load(f) - - # Untar the tarball to a temporary directory - self.temp_dir = tempfile.mkdtemp( - dir=os.path.join(self.chromeos_root, 'chroot', 'tmp')) - - command = ('cd %s && tar xf %s' % - (self.temp_dir, os.path.join(cache_dir, AUTOTEST_TARBALL))) - ret = self.ce.RunCommand(command, print_to_console=False) - if ret: - raise RuntimeError('Could not untar cached tarball') - self.results_dir = self.temp_dir - self.results_file = self.GetDataMeasurementsFiles() - self.perf_data_files = self.GetPerfDataFiles() - self.perf_report_files = self.GetPerfReportFiles() - self.chrome_version = self.GetChromeVersionFromCache(cache_dir) - self.ProcessResults(use_cache=True) - - def CleanUp(self, rm_chroot_tmp): - if rm_chroot_tmp and self.results_dir: - dirname, basename = misc.GetRoot(self.results_dir) - if basename.find('test_that_results_') != -1: - command = 'rm -rf %s' % self.results_dir - else: - command = 'rm -rf %s' % dirname - self.ce.RunCommand(command) - if self.temp_dir: - command = 'rm -rf %s' % self.temp_dir - self.ce.RunCommand(command) - - def CreateTarball(self, results_dir, tarball): - if not results_dir.strip(): - raise ValueError('Refusing to `tar` an empty results_dir: %r' % - results_dir) - - ret = self.ce.RunCommand('cd %s && ' - 'tar ' - '--exclude=var/spool ' - '--exclude=var/log ' - '-cjf %s .' % (results_dir, tarball)) - if ret: - raise RuntimeError("Couldn't compress test output directory.") - - def StoreToCacheDir(self, cache_dir, machine_manager, key_list): - # Create the dir if it doesn't exist. - temp_dir = tempfile.mkdtemp() - - # Store to the temp directory. - with open(os.path.join(temp_dir, RESULTS_FILE), 'wb') as f: - pickle.dump(self.out, f) - pickle.dump(self.err, f) - pickle.dump(self.retval, f) - - if not test_flag.GetTestMode(): - with open(os.path.join(temp_dir, CACHE_KEYS_FILE), 'w') as f: - f.write('%s\n' % self.label.name) - f.write('%s\n' % self.label.chrome_version) - f.write('%s\n' % self.machine.checksum_string) - for k in key_list: - f.write(k) - f.write('\n') - - if self.results_dir: - tarball = os.path.join(temp_dir, AUTOTEST_TARBALL) - self.CreateTarball(self.results_dir, tarball) - - # Store machine info. - # TODO(asharif): Make machine_manager a singleton, and don't pass it into - # this function. - with open(os.path.join(temp_dir, MACHINE_FILE), 'w') as f: - f.write(machine_manager.machine_checksum_string[self.label.name]) - - if os.path.exists(cache_dir): - command = f'rm -rf {cache_dir}' - self.ce.RunCommand(command) - - parent_dir = os.path.dirname(cache_dir) - command = f'mkdir -p {parent_dir} && ' - command += f'chmod g+x {temp_dir} && ' - command += f'mv {temp_dir} {cache_dir}' - ret = self.ce.RunCommand(command) - if ret: - command = f'rm -rf {temp_dir}' - self.ce.RunCommand(command) - raise RuntimeError('Could not move dir %s to dir %s' % - (temp_dir, cache_dir)) - - @classmethod - def CreateFromRun(cls, - logger, - log_level, - label, - machine, - out, - err, - retval, - test, - suite='telemetry_Crosperf', - cwp_dso=''): - if suite == 'telemetry': - result = TelemetryResult(logger, label, log_level, machine) - else: - result = cls(logger, label, log_level, machine) - result.PopulateFromRun(out, err, retval, test, suite, cwp_dso) - return result - - @classmethod - def CreateFromCacheHit(cls, - logger, - log_level, - label, - machine, - cache_dir, - test, - suite='telemetry_Crosperf', - cwp_dso=''): - if suite == 'telemetry': - result = TelemetryResult(logger, label, log_level, machine) - else: - result = cls(logger, label, log_level, machine) - try: - result.PopulateFromCacheDir(cache_dir, test, suite, cwp_dso) - - except RuntimeError as e: - logger.LogError('Exception while using cache: %s' % e) - return None - return result + result.PopulateFromCacheDir(cache_dir, test, suite, cwp_dso) + + except RuntimeError as e: + logger.LogError("Exception while using cache: %s" % e) + return None + return result class TelemetryResult(Result): - """Class to hold the results of a single Telemetry run.""" - - def PopulateFromRun(self, out, err, retval, test, suite, cwp_dso): - self.out = out - self.err = err - self.retval = retval - - self.ProcessResults() - - # pylint: disable=arguments-differ - def ProcessResults(self): - # The output is: - # url,average_commit_time (ms),... - # www.google.com,33.4,21.2,... - # We need to convert to this format: - # {"www.google.com:average_commit_time (ms)": "33.4", - # "www.google.com:...": "21.2"} - # Added note: Occasionally the output comes back - # with "JSON.stringify(window.automation.GetResults())" on - # the first line, and then the rest of the output as - # described above. - - lines = self.out.splitlines() - self.keyvals = {} - - if lines: - if lines[0].startswith('JSON.stringify'): - lines = lines[1:] - - if not lines: - return - labels = lines[0].split(',') - for line in lines[1:]: - fields = line.split(',') - if len(fields) != len(labels): - continue - for i in range(1, len(labels)): - key = '%s %s' % (fields[0], labels[i]) - value = fields[i] - self.keyvals[key] = value - self.keyvals['retval'] = self.retval - - def PopulateFromCacheDir(self, cache_dir, test, suite, cwp_dso): - self.test_name = test - self.suite = suite - self.cwp_dso = cwp_dso - with open(os.path.join(cache_dir, RESULTS_FILE), 'rb') as f: - self.out = pickle.load(f) - self.err = pickle.load(f) - self.retval = pickle.load(f) - - self.chrome_version = (super(TelemetryResult, - self).GetChromeVersionFromCache(cache_dir)) - self.ProcessResults() + """Class to hold the results of a single Telemetry run.""" + + def PopulateFromRun(self, out, err, retval, test, suite, cwp_dso): + self.out = out + self.err = err + self.retval = retval + + self.ProcessResults() + + # pylint: disable=arguments-differ + def ProcessResults(self): + # The output is: + # url,average_commit_time (ms),... + # www.google.com,33.4,21.2,... + # We need to convert to this format: + # {"www.google.com:average_commit_time (ms)": "33.4", + # "www.google.com:...": "21.2"} + # Added note: Occasionally the output comes back + # with "JSON.stringify(window.automation.GetResults())" on + # the first line, and then the rest of the output as + # described above. + + lines = self.out.splitlines() + self.keyvals = {} + + if lines: + if lines[0].startswith("JSON.stringify"): + lines = lines[1:] + + if not lines: + return + labels = lines[0].split(",") + for line in lines[1:]: + fields = line.split(",") + if len(fields) != len(labels): + continue + for i in range(1, len(labels)): + key = "%s %s" % (fields[0], labels[i]) + value = fields[i] + self.keyvals[key] = value + self.keyvals["retval"] = self.retval + + def PopulateFromCacheDir(self, cache_dir, test, suite, cwp_dso): + self.test_name = test + self.suite = suite + self.cwp_dso = cwp_dso + with open(os.path.join(cache_dir, RESULTS_FILE), "rb") as f: + self.out = pickle.load(f) + self.err = pickle.load(f) + self.retval = pickle.load(f) + + self.chrome_version = super( + TelemetryResult, self + ).GetChromeVersionFromCache(cache_dir) + self.ProcessResults() class CacheConditions(object): - """Various Cache condition values, for export.""" + """Various Cache condition values, for export.""" - # Cache hit only if the result file exists. - CACHE_FILE_EXISTS = 0 + # Cache hit only if the result file exists. + CACHE_FILE_EXISTS = 0 - # Cache hit if the checksum of cpuinfo and totalmem of - # the cached result and the new run match. - MACHINES_MATCH = 1 + # Cache hit if the checksum of cpuinfo and totalmem of + # the cached result and the new run match. + MACHINES_MATCH = 1 - # Cache hit if the image checksum of the cached result and the new run match. - CHECKSUMS_MATCH = 2 + # Cache hit if the image checksum of the cached result and the new run match. + CHECKSUMS_MATCH = 2 - # Cache hit only if the cached result was successful - RUN_SUCCEEDED = 3 + # Cache hit only if the cached result was successful + RUN_SUCCEEDED = 3 - # Never a cache hit. - FALSE = 4 + # Never a cache hit. + FALSE = 4 - # Cache hit if the image path matches the cached image path. - IMAGE_PATH_MATCH = 5 + # Cache hit if the image path matches the cached image path. + IMAGE_PATH_MATCH = 5 - # Cache hit if the uuid of hard disk mataches the cached one + # Cache hit if the uuid of hard disk mataches the cached one - SAME_MACHINE_MATCH = 6 + SAME_MACHINE_MATCH = 6 class ResultsCache(object): - """Class to handle the cache for storing/retrieving test run results. - - This class manages the key of the cached runs without worrying about what - is exactly stored (value). The value generation is handled by the Results - class. - """ - CACHE_VERSION = 6 - - def __init__(self): - # Proper initialization happens in the Init function below. - self.chromeos_image = None - self.chromeos_root = None - self.test_name = None - self.iteration = None - self.test_args = None - self.profiler_args = None - self.board = None - self.cache_conditions = None - self.machine_manager = None - self.machine = None - self._logger = None - self.ce = None - self.label = None - self.share_cache = None - self.suite = None - self.log_level = None - self.show_all = None - self.run_local = None - self.cwp_dso = None - - def Init(self, chromeos_image, chromeos_root, test_name, iteration, - test_args, profiler_args, machine_manager, machine, board, - cache_conditions, logger_to_use, log_level, label, share_cache, - suite, show_all_results, run_local, cwp_dso): - self.chromeos_image = chromeos_image - self.chromeos_root = chromeos_root - self.test_name = test_name - self.iteration = iteration - self.test_args = test_args - self.profiler_args = profiler_args - self.board = board - self.cache_conditions = cache_conditions - self.machine_manager = machine_manager - self.machine = machine - self._logger = logger_to_use - self.ce = command_executer.GetCommandExecuter(self._logger, - log_level=log_level) - self.label = label - self.share_cache = share_cache - self.suite = suite - self.log_level = log_level - self.show_all = show_all_results - self.run_local = run_local - self.cwp_dso = cwp_dso - - def GetCacheDirForRead(self): - matching_dirs = [] - for glob_path in self.FormCacheDir(self.GetCacheKeyList(True)): - matching_dirs += glob.glob(glob_path) - - if matching_dirs: - # Cache file found. - return matching_dirs[0] - return None - - def GetCacheDirForWrite(self, get_keylist=False): - cache_path = self.FormCacheDir(self.GetCacheKeyList(False))[0] - if get_keylist: - args_str = '%s_%s_%s' % (self.test_args, self.profiler_args, - self.run_local) - version, image = results_report.ParseChromeosImage( - self.label.chromeos_image) - keylist = [ - version, image, self.label.board, self.machine.name, self.test_name, - str(self.iteration), args_str - ] - return cache_path, keylist - return cache_path - - def FormCacheDir(self, list_of_strings): - cache_key = ' '.join(list_of_strings) - cache_dir = misc.GetFilenameFromString(cache_key) - if self.label.cache_dir: - cache_home = os.path.abspath(os.path.expanduser(self.label.cache_dir)) - cache_path = [os.path.join(cache_home, cache_dir)] - else: - cache_path = [os.path.join(SCRATCH_DIR, cache_dir)] - - if self.share_cache: - for path in [x.strip() for x in self.share_cache.split(',')]: - if os.path.exists(path): - cache_path.append(os.path.join(path, cache_dir)) + """Class to handle the cache for storing/retrieving test run results. + + This class manages the key of the cached runs without worrying about what + is exactly stored (value). The value generation is handled by the Results + class. + """ + + CACHE_VERSION = 6 + + def __init__(self): + # Proper initialization happens in the Init function below. + self.chromeos_image = None + self.chromeos_root = None + self.test_name = None + self.iteration = None + self.test_args = None + self.profiler_args = None + self.board = None + self.cache_conditions = None + self.machine_manager = None + self.machine = None + self._logger = None + self.ce = None + self.label = None + self.share_cache = None + self.suite = None + self.log_level = None + self.show_all = None + self.run_local = None + self.cwp_dso = None + + def Init( + self, + chromeos_image, + chromeos_root, + test_name, + iteration, + test_args, + profiler_args, + machine_manager, + machine, + board, + cache_conditions, + logger_to_use, + log_level, + label, + share_cache, + suite, + show_all_results, + run_local, + cwp_dso, + ): + self.chromeos_image = chromeos_image + self.chromeos_root = chromeos_root + self.test_name = test_name + self.iteration = iteration + self.test_args = test_args + self.profiler_args = profiler_args + self.board = board + self.cache_conditions = cache_conditions + self.machine_manager = machine_manager + self.machine = machine + self._logger = logger_to_use + self.ce = command_executer.GetCommandExecuter( + self._logger, log_level=log_level + ) + self.label = label + self.share_cache = share_cache + self.suite = suite + self.log_level = log_level + self.show_all = show_all_results + self.run_local = run_local + self.cwp_dso = cwp_dso + + def GetCacheDirForRead(self): + matching_dirs = [] + for glob_path in self.FormCacheDir(self.GetCacheKeyList(True)): + matching_dirs += glob.glob(glob_path) + + if matching_dirs: + # Cache file found. + return matching_dirs[0] + return None + + def GetCacheDirForWrite(self, get_keylist=False): + cache_path = self.FormCacheDir(self.GetCacheKeyList(False))[0] + if get_keylist: + args_str = "%s_%s_%s" % ( + self.test_args, + self.profiler_args, + self.run_local, + ) + version, image = results_report.ParseChromeosImage( + self.label.chromeos_image + ) + keylist = [ + version, + image, + self.label.board, + self.machine.name, + self.test_name, + str(self.iteration), + args_str, + ] + return cache_path, keylist + return cache_path + + def FormCacheDir(self, list_of_strings): + cache_key = " ".join(list_of_strings) + cache_dir = misc.GetFilenameFromString(cache_key) + if self.label.cache_dir: + cache_home = os.path.abspath( + os.path.expanduser(self.label.cache_dir) + ) + cache_path = [os.path.join(cache_home, cache_dir)] + else: + cache_path = [os.path.join(SCRATCH_DIR, cache_dir)] + + if self.share_cache: + for path in [x.strip() for x in self.share_cache.split(",")]: + if os.path.exists(path): + cache_path.append(os.path.join(path, cache_dir)) + else: + self._logger.LogFatal( + "Unable to find shared cache: %s" % path + ) + + return cache_path + + def GetCacheKeyList(self, read): + if read and CacheConditions.MACHINES_MATCH not in self.cache_conditions: + machine_checksum = "*" + else: + machine_checksum = self.machine_manager.machine_checksum[ + self.label.name + ] + if ( + read + and CacheConditions.CHECKSUMS_MATCH not in self.cache_conditions + ): + checksum = "*" + elif self.label.image_type == "trybot": + checksum = hashlib.md5( + self.label.chromeos_image.encode("utf-8") + ).hexdigest() + elif self.label.image_type == "official": + checksum = "*" else: - self._logger.LogFatal('Unable to find shared cache: %s' % path) - - return cache_path - - def GetCacheKeyList(self, read): - if read and CacheConditions.MACHINES_MATCH not in self.cache_conditions: - machine_checksum = '*' - else: - machine_checksum = self.machine_manager.machine_checksum[self.label.name] - if read and CacheConditions.CHECKSUMS_MATCH not in self.cache_conditions: - checksum = '*' - elif self.label.image_type == 'trybot': - checksum = hashlib.md5( - self.label.chromeos_image.encode('utf-8')).hexdigest() - elif self.label.image_type == 'official': - checksum = '*' - else: - checksum = ImageChecksummer().Checksum(self.label, self.log_level) - - if read and CacheConditions.IMAGE_PATH_MATCH not in self.cache_conditions: - image_path_checksum = '*' - else: - image_path_checksum = hashlib.md5( - self.chromeos_image.encode('utf-8')).hexdigest() - - machine_id_checksum = '' - if read and CacheConditions.SAME_MACHINE_MATCH not in self.cache_conditions: - machine_id_checksum = '*' - else: - if self.machine and self.machine.name in self.label.remote: - machine_id_checksum = self.machine.machine_id_checksum - else: - for machine in self.machine_manager.GetMachines(self.label): - if machine.name == self.label.remote[0]: - machine_id_checksum = machine.machine_id_checksum - break - - temp_test_args = '%s %s %s' % (self.test_args, self.profiler_args, - self.run_local) - test_args_checksum = hashlib.md5( - temp_test_args.encode('utf-8')).hexdigest() - return (image_path_checksum, self.test_name, str(self.iteration), - test_args_checksum, checksum, machine_checksum, - machine_id_checksum, str(self.CACHE_VERSION)) - - def ReadResult(self): - if CacheConditions.FALSE in self.cache_conditions: - cache_dir = self.GetCacheDirForWrite() - command = 'rm -rf %s' % (cache_dir, ) - self.ce.RunCommand(command) - return None - cache_dir = self.GetCacheDirForRead() - - if not cache_dir: - return None - - if not os.path.isdir(cache_dir): - return None - - if self.log_level == 'verbose': - self._logger.LogOutput('Trying to read from cache dir: %s' % cache_dir) - result = Result.CreateFromCacheHit(self._logger, self.log_level, - self.label, self.machine, cache_dir, - self.test_name, self.suite, - self.cwp_dso) - if not result: - return None - - if (result.retval == 0 - or CacheConditions.RUN_SUCCEEDED not in self.cache_conditions): - return result - - return None - - def StoreResult(self, result): - cache_dir, keylist = self.GetCacheDirForWrite(get_keylist=True) - result.StoreToCacheDir(cache_dir, self.machine_manager, keylist) + checksum = ImageChecksummer().Checksum(self.label, self.log_level) + + if ( + read + and CacheConditions.IMAGE_PATH_MATCH not in self.cache_conditions + ): + image_path_checksum = "*" + else: + image_path_checksum = hashlib.md5( + self.chromeos_image.encode("utf-8") + ).hexdigest() + + machine_id_checksum = "" + if ( + read + and CacheConditions.SAME_MACHINE_MATCH not in self.cache_conditions + ): + machine_id_checksum = "*" + else: + if self.machine and self.machine.name in self.label.remote: + machine_id_checksum = self.machine.machine_id_checksum + else: + for machine in self.machine_manager.GetMachines(self.label): + if machine.name == self.label.remote[0]: + machine_id_checksum = machine.machine_id_checksum + break + + temp_test_args = "%s %s %s" % ( + self.test_args, + self.profiler_args, + self.run_local, + ) + test_args_checksum = hashlib.md5( + temp_test_args.encode("utf-8") + ).hexdigest() + return ( + image_path_checksum, + self.test_name, + str(self.iteration), + test_args_checksum, + checksum, + machine_checksum, + machine_id_checksum, + str(self.CACHE_VERSION), + ) + + def ReadResult(self): + if CacheConditions.FALSE in self.cache_conditions: + cache_dir = self.GetCacheDirForWrite() + command = "rm -rf %s" % (cache_dir,) + self.ce.RunCommand(command) + return None + cache_dir = self.GetCacheDirForRead() + + if not cache_dir: + return None + + if not os.path.isdir(cache_dir): + return None + + if self.log_level == "verbose": + self._logger.LogOutput( + "Trying to read from cache dir: %s" % cache_dir + ) + result = Result.CreateFromCacheHit( + self._logger, + self.log_level, + self.label, + self.machine, + cache_dir, + self.test_name, + self.suite, + self.cwp_dso, + ) + if not result: + return None + + if ( + result.retval == 0 + or CacheConditions.RUN_SUCCEEDED not in self.cache_conditions + ): + return result + + return None + + def StoreResult(self, result): + cache_dir, keylist = self.GetCacheDirForWrite(get_keylist=True) + result.StoreToCacheDir(cache_dir, self.machine_manager, keylist) class MockResultsCache(ResultsCache): - """Class for mock testing, corresponding to ResultsCache class.""" + """Class for mock testing, corresponding to ResultsCache class.""" - # FIXME: pylint complains about this mock init method, we should probably - # replace all Mock classes in Crosperf with simple Mock.mock(). - # pylint: disable=arguments-differ - def Init(self, *args): - pass + # FIXME: pylint complains about this mock init method, we should probably + # replace all Mock classes in Crosperf with simple Mock.mock(). + # pylint: disable=arguments-differ + def Init(self, *args): + pass - def ReadResult(self): - return None + def ReadResult(self): + return None - def StoreResult(self, result): - pass + def StoreResult(self, result): + pass class MockResult(Result): - """Class for mock testing, corresponding to Result class.""" + """Class for mock testing, corresponding to Result class.""" - def PopulateFromRun(self, out, err, retval, test, suite, cwp_dso): - self.out = out - self.err = err - self.retval = retval + def PopulateFromRun(self, out, err, retval, test, suite, cwp_dso): + self.out = out + self.err = err + self.retval = retval |