1*760c253cSXin Li# -*- coding: utf-8 -*- 2*760c253cSXin Li# Copyright 2013 The ChromiumOS Authors 3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 4*760c253cSXin Li# found in the LICENSE file. 5*760c253cSXin Li 6*760c253cSXin Li"""Module to deal with result cache.""" 7*760c253cSXin Li 8*760c253cSXin Li 9*760c253cSXin Liimport collections 10*760c253cSXin Liimport glob 11*760c253cSXin Liimport hashlib 12*760c253cSXin Liimport heapq 13*760c253cSXin Liimport json 14*760c253cSXin Liimport os 15*760c253cSXin Liimport pickle 16*760c253cSXin Liimport re 17*760c253cSXin Liimport tempfile 18*760c253cSXin Li 19*760c253cSXin Lifrom cros_utils import command_executer 20*760c253cSXin Lifrom cros_utils import misc 21*760c253cSXin Lifrom image_checksummer import ImageChecksummer 22*760c253cSXin Liimport results_report 23*760c253cSXin Liimport test_flag 24*760c253cSXin Li 25*760c253cSXin Li 26*760c253cSXin LiSCRATCH_DIR = os.path.expanduser("~/cros_scratch") 27*760c253cSXin LiRESULTS_FILE = "results.pickle" 28*760c253cSXin LiMACHINE_FILE = "machine.txt" 29*760c253cSXin LiAUTOTEST_TARBALL = "autotest.tbz2" 30*760c253cSXin LiRESULTS_TARBALL = "results.tbz2" 31*760c253cSXin LiPERF_RESULTS_FILE = "perf-results.txt" 32*760c253cSXin LiCACHE_KEYS_FILE = "cache_keys.txt" 33*760c253cSXin Li 34*760c253cSXin Li 35*760c253cSXin Liclass PidVerificationError(Exception): 36*760c253cSXin Li """Error of perf PID verification in per-process mode.""" 37*760c253cSXin Li 38*760c253cSXin Li 39*760c253cSXin Liclass PerfDataReadError(Exception): 40*760c253cSXin Li """Error of reading a perf.data header.""" 41*760c253cSXin Li 42*760c253cSXin Li 43*760c253cSXin Liclass Result(object): 44*760c253cSXin Li """Class for holding the results of a single test run. 45*760c253cSXin Li 46*760c253cSXin Li This class manages what exactly is stored inside the cache without knowing 47*760c253cSXin Li what the key of the cache is. For runs with perf, it stores perf.data, 48*760c253cSXin Li perf.report, etc. The key generation is handled by the ResultsCache class. 49*760c253cSXin Li """ 50*760c253cSXin Li 51*760c253cSXin Li def __init__(self, logger, label, log_level, machine, cmd_exec=None): 52*760c253cSXin Li self.chromeos_root = label.chromeos_root 53*760c253cSXin Li self._logger = logger 54*760c253cSXin Li self.ce = cmd_exec or command_executer.GetCommandExecuter( 55*760c253cSXin Li self._logger, log_level=log_level 56*760c253cSXin Li ) 57*760c253cSXin Li self.temp_dir = None 58*760c253cSXin Li self.label = label 59*760c253cSXin Li self.results_dir = None 60*760c253cSXin Li self.log_level = log_level 61*760c253cSXin Li self.machine = machine 62*760c253cSXin Li self.perf_data_files = [] 63*760c253cSXin Li self.perf_report_files = [] 64*760c253cSXin Li self.results_file = [] 65*760c253cSXin Li self.turbostat_log_file = "" 66*760c253cSXin Li self.cpustats_log_file = "" 67*760c253cSXin Li self.cpuinfo_file = "" 68*760c253cSXin Li self.top_log_file = "" 69*760c253cSXin Li self.wait_time_log_file = "" 70*760c253cSXin Li self.chrome_version = "" 71*760c253cSXin Li self.err = None 72*760c253cSXin Li self.chroot_results_dir = "" 73*760c253cSXin Li self.test_name = "" 74*760c253cSXin Li self.keyvals = None 75*760c253cSXin Li self.board = None 76*760c253cSXin Li self.suite = None 77*760c253cSXin Li self.cwp_dso = "" 78*760c253cSXin Li self.retval = None 79*760c253cSXin Li self.out = None 80*760c253cSXin Li self.top_cmds = [] 81*760c253cSXin Li 82*760c253cSXin Li def GetTopCmds(self): 83*760c253cSXin Li """Get the list of top commands consuming CPU on the machine.""" 84*760c253cSXin Li return self.top_cmds 85*760c253cSXin Li 86*760c253cSXin Li def FormatStringTopCommands(self): 87*760c253cSXin Li """Get formatted string of top commands. 88*760c253cSXin Li 89*760c253cSXin Li Get the formatted string with top commands consuming CPU on DUT machine. 90*760c253cSXin Li Number of "non-chrome" processes in the list is limited to 5. 91*760c253cSXin Li """ 92*760c253cSXin Li format_list = [ 93*760c253cSXin Li "Top commands with highest CPU usage:", 94*760c253cSXin Li # Header. 95*760c253cSXin Li "%20s %9s %6s %s" % ("COMMAND", "AVG CPU%", "COUNT", "HIGHEST 5"), 96*760c253cSXin Li "-" * 50, 97*760c253cSXin Li ] 98*760c253cSXin Li if self.top_cmds: 99*760c253cSXin Li # After switching to top processes we have to expand the list since there 100*760c253cSXin Li # will be a lot of 'chrome' processes (up to 10, sometimes more) in the 101*760c253cSXin Li # top. 102*760c253cSXin Li # Let's limit the list size by the number of non-chrome processes. 103*760c253cSXin Li limit_of_non_chrome_procs = 5 104*760c253cSXin Li num_of_non_chrome_procs = 0 105*760c253cSXin Li for topcmd in self.top_cmds: 106*760c253cSXin Li print_line = "%20s %9.2f %6s %s" % ( 107*760c253cSXin Li topcmd["cmd"], 108*760c253cSXin Li topcmd["cpu_use_avg"], 109*760c253cSXin Li topcmd["count"], 110*760c253cSXin Li topcmd["top5_cpu_use"], 111*760c253cSXin Li ) 112*760c253cSXin Li format_list.append(print_line) 113*760c253cSXin Li if not topcmd["cmd"].startswith("chrome"): 114*760c253cSXin Li num_of_non_chrome_procs += 1 115*760c253cSXin Li if num_of_non_chrome_procs >= limit_of_non_chrome_procs: 116*760c253cSXin Li break 117*760c253cSXin Li else: 118*760c253cSXin Li format_list.append("[NO DATA FROM THE TOP LOG]") 119*760c253cSXin Li format_list.append("-" * 50) 120*760c253cSXin Li return "\n".join(format_list) 121*760c253cSXin Li 122*760c253cSXin Li def CopyFilesTo(self, dest_dir, files_to_copy): 123*760c253cSXin Li file_index = 0 124*760c253cSXin Li for file_to_copy in files_to_copy: 125*760c253cSXin Li if not os.path.isdir(dest_dir): 126*760c253cSXin Li command = "mkdir -p %s" % dest_dir 127*760c253cSXin Li self.ce.RunCommand(command) 128*760c253cSXin Li dest_file = os.path.join( 129*760c253cSXin Li dest_dir, 130*760c253cSXin Li ("%s.%s" % (os.path.basename(file_to_copy), file_index)), 131*760c253cSXin Li ) 132*760c253cSXin Li ret = self.ce.CopyFiles(file_to_copy, dest_file, recursive=False) 133*760c253cSXin Li if ret: 134*760c253cSXin Li raise IOError("Could not copy results file: %s" % file_to_copy) 135*760c253cSXin Li file_index += 1 136*760c253cSXin Li 137*760c253cSXin Li def CopyResultsTo(self, dest_dir): 138*760c253cSXin Li self.CopyFilesTo(dest_dir, self.results_file) 139*760c253cSXin Li self.CopyFilesTo(dest_dir, self.perf_data_files) 140*760c253cSXin Li self.CopyFilesTo(dest_dir, self.perf_report_files) 141*760c253cSXin Li extra_files = [] 142*760c253cSXin Li if self.top_log_file: 143*760c253cSXin Li extra_files.append(self.top_log_file) 144*760c253cSXin Li if self.cpuinfo_file: 145*760c253cSXin Li extra_files.append(self.cpuinfo_file) 146*760c253cSXin Li if extra_files: 147*760c253cSXin Li self.CopyFilesTo(dest_dir, extra_files) 148*760c253cSXin Li if self.results_file or self.perf_data_files or self.perf_report_files: 149*760c253cSXin Li self._logger.LogOutput("Results files stored in %s." % dest_dir) 150*760c253cSXin Li 151*760c253cSXin Li def CompressResultsTo(self, dest_dir): 152*760c253cSXin Li tarball = os.path.join(self.results_dir, RESULTS_TARBALL) 153*760c253cSXin Li # Test_that runs hold all output under TEST_NAME_HASHTAG/results/, 154*760c253cSXin Li # while tast runs hold output under TEST_NAME/. 155*760c253cSXin Li # Both ensure to be unique. 156*760c253cSXin Li result_dir_name = self.test_name if self.suite == "tast" else "results" 157*760c253cSXin Li results_dir = self.FindFilesInResultsDir( 158*760c253cSXin Li "-name %s" % result_dir_name 159*760c253cSXin Li ).split("\n")[0] 160*760c253cSXin Li 161*760c253cSXin Li if not results_dir: 162*760c253cSXin Li self._logger.LogOutput( 163*760c253cSXin Li "WARNING: No results dir matching %r found" % result_dir_name 164*760c253cSXin Li ) 165*760c253cSXin Li return 166*760c253cSXin Li 167*760c253cSXin Li self.CreateTarball(results_dir, tarball) 168*760c253cSXin Li self.CopyFilesTo(dest_dir, [tarball]) 169*760c253cSXin Li if results_dir: 170*760c253cSXin Li self._logger.LogOutput( 171*760c253cSXin Li "Results files compressed into %s." % dest_dir 172*760c253cSXin Li ) 173*760c253cSXin Li 174*760c253cSXin Li def GetNewKeyvals(self, keyvals_dict): 175*760c253cSXin Li # Initialize 'units' dictionary. 176*760c253cSXin Li units_dict = {} 177*760c253cSXin Li for k in keyvals_dict: 178*760c253cSXin Li units_dict[k] = "" 179*760c253cSXin Li results_files = self.GetDataMeasurementsFiles() 180*760c253cSXin Li for f in results_files: 181*760c253cSXin Li # Make sure we can find the results file 182*760c253cSXin Li if os.path.exists(f): 183*760c253cSXin Li data_filename = f 184*760c253cSXin Li else: 185*760c253cSXin Li # Otherwise get the base filename and create the correct 186*760c253cSXin Li # path for it. 187*760c253cSXin Li _, f_base = misc.GetRoot(f) 188*760c253cSXin Li data_filename = misc.GetOutsideChrootPath( 189*760c253cSXin Li self.chromeos_root, 190*760c253cSXin Li os.path.join("/tmp", self.temp_dir, f_base), 191*760c253cSXin Li ) 192*760c253cSXin Li if data_filename.find(".json") > 0: 193*760c253cSXin Li raw_dict = dict() 194*760c253cSXin Li if os.path.exists(data_filename): 195*760c253cSXin Li with open( 196*760c253cSXin Li data_filename, "r", encoding="utf-8" 197*760c253cSXin Li ) as data_file: 198*760c253cSXin Li raw_dict = json.load(data_file) 199*760c253cSXin Li 200*760c253cSXin Li if "charts" in raw_dict: 201*760c253cSXin Li raw_dict = raw_dict["charts"] 202*760c253cSXin Li for k1 in raw_dict: 203*760c253cSXin Li field_dict = raw_dict[k1] 204*760c253cSXin Li for k2 in field_dict: 205*760c253cSXin Li result_dict = field_dict[k2] 206*760c253cSXin Li key = k1 + "__" + k2 207*760c253cSXin Li if "value" in result_dict: 208*760c253cSXin Li keyvals_dict[key] = result_dict["value"] 209*760c253cSXin Li elif "values" in result_dict: 210*760c253cSXin Li values = result_dict["values"] 211*760c253cSXin Li if ( 212*760c253cSXin Li "type" in result_dict 213*760c253cSXin Li and result_dict["type"] 214*760c253cSXin Li == "list_of_scalar_values" 215*760c253cSXin Li and values 216*760c253cSXin Li and values != "null" 217*760c253cSXin Li ): 218*760c253cSXin Li keyvals_dict[key] = sum(values) / float( 219*760c253cSXin Li len(values) 220*760c253cSXin Li ) 221*760c253cSXin Li else: 222*760c253cSXin Li keyvals_dict[key] = values 223*760c253cSXin Li units_dict[key] = result_dict["units"] 224*760c253cSXin Li else: 225*760c253cSXin Li if os.path.exists(data_filename): 226*760c253cSXin Li with open( 227*760c253cSXin Li data_filename, "r", encoding="utf-8" 228*760c253cSXin Li ) as data_file: 229*760c253cSXin Li lines = data_file.readlines() 230*760c253cSXin Li for line in lines: 231*760c253cSXin Li tmp_dict = json.loads(line) 232*760c253cSXin Li graph_name = tmp_dict["graph"] 233*760c253cSXin Li graph_str = ( 234*760c253cSXin Li (graph_name + "__") if graph_name else "" 235*760c253cSXin Li ) 236*760c253cSXin Li key = graph_str + tmp_dict["description"] 237*760c253cSXin Li keyvals_dict[key] = tmp_dict["value"] 238*760c253cSXin Li units_dict[key] = tmp_dict["units"] 239*760c253cSXin Li 240*760c253cSXin Li return keyvals_dict, units_dict 241*760c253cSXin Li 242*760c253cSXin Li def AppendTelemetryUnits(self, keyvals_dict, units_dict): 243*760c253cSXin Li """keyvals_dict is the dict of key-value used to generate Crosperf reports. 244*760c253cSXin Li 245*760c253cSXin Li units_dict is a dictionary of the units for the return values in 246*760c253cSXin Li keyvals_dict. We need to associate the units with the return values, 247*760c253cSXin Li for Telemetry tests, so that we can include the units in the reports. 248*760c253cSXin Li This function takes each value in keyvals_dict, finds the corresponding 249*760c253cSXin Li unit in the units_dict, and replaces the old value with a list of the 250*760c253cSXin Li old value and the units. This later gets properly parsed in the 251*760c253cSXin Li ResultOrganizer class, for generating the reports. 252*760c253cSXin Li """ 253*760c253cSXin Li 254*760c253cSXin Li results_dict = {} 255*760c253cSXin Li for k in keyvals_dict: 256*760c253cSXin Li # We don't want these lines in our reports; they add no useful data. 257*760c253cSXin Li if not k or k == "telemetry_Crosperf": 258*760c253cSXin Li continue 259*760c253cSXin Li val = keyvals_dict[k] 260*760c253cSXin Li units = units_dict[k] 261*760c253cSXin Li new_val = [val, units] 262*760c253cSXin Li results_dict[k] = new_val 263*760c253cSXin Li return results_dict 264*760c253cSXin Li 265*760c253cSXin Li def GetKeyvals(self): 266*760c253cSXin Li results_in_chroot = misc.GetOutsideChrootPath( 267*760c253cSXin Li self.chromeos_root, "/tmp" 268*760c253cSXin Li ) 269*760c253cSXin Li if not self.temp_dir: 270*760c253cSXin Li self.temp_dir = tempfile.mkdtemp(dir=results_in_chroot) 271*760c253cSXin Li command = f"cp -r {self.results_dir}/* {self.temp_dir}" 272*760c253cSXin Li self.ce.RunCommand(command, print_to_console=False) 273*760c253cSXin Li 274*760c253cSXin Li tmp_dir_in_chroot = misc.GetInsideChrootPath( 275*760c253cSXin Li self.chromeos_root, self.temp_dir 276*760c253cSXin Li ) 277*760c253cSXin Li command = "./generate_test_report --no-color --csv %s" % ( 278*760c253cSXin Li tmp_dir_in_chroot 279*760c253cSXin Li ) 280*760c253cSXin Li _, out, _ = self.ce.ChrootRunCommandWOutput( 281*760c253cSXin Li self.chromeos_root, command, print_to_console=False 282*760c253cSXin Li ) 283*760c253cSXin Li keyvals_dict = {} 284*760c253cSXin Li for line in out.splitlines(): 285*760c253cSXin Li tokens = re.split("=|,", line) 286*760c253cSXin Li key = tokens[-2] 287*760c253cSXin Li if key.startswith(tmp_dir_in_chroot): 288*760c253cSXin Li key = key[len(tmp_dir_in_chroot) + 1 :] 289*760c253cSXin Li value = tokens[-1] 290*760c253cSXin Li keyvals_dict[key] = value 291*760c253cSXin Li 292*760c253cSXin Li # Check to see if there is a perf_measurements file and get the 293*760c253cSXin Li # data from it if so. 294*760c253cSXin Li keyvals_dict, units_dict = self.GetNewKeyvals(keyvals_dict) 295*760c253cSXin Li if self.suite == "telemetry_Crosperf": 296*760c253cSXin Li # For telemtry_Crosperf results, append the units to the return 297*760c253cSXin Li # results, for use in generating the reports. 298*760c253cSXin Li keyvals_dict = self.AppendTelemetryUnits(keyvals_dict, units_dict) 299*760c253cSXin Li return keyvals_dict 300*760c253cSXin Li 301*760c253cSXin Li def GetSamples(self): 302*760c253cSXin Li actual_samples = 0 303*760c253cSXin Li for perf_data_file in self.perf_data_files: 304*760c253cSXin Li chroot_perf_data_file = misc.GetInsideChrootPath( 305*760c253cSXin Li self.chromeos_root, perf_data_file 306*760c253cSXin Li ) 307*760c253cSXin Li perf_path = misc.GetOutsideChrootPath( 308*760c253cSXin Li self.chromeos_root, "/usr/bin/perf" 309*760c253cSXin Li ) 310*760c253cSXin Li perf_file = "/usr/sbin/perf" 311*760c253cSXin Li if os.path.exists(perf_path): 312*760c253cSXin Li perf_file = "/usr/bin/perf" 313*760c253cSXin Li 314*760c253cSXin Li # For each perf.data, we want to collect sample count for specific DSO. 315*760c253cSXin Li # We specify exact match for known DSO type, and every sample for `all`. 316*760c253cSXin Li exact_match = "" 317*760c253cSXin Li if self.cwp_dso == "all": 318*760c253cSXin Li exact_match = "" 319*760c253cSXin Li elif self.cwp_dso == "chrome": 320*760c253cSXin Li exact_match = "chrome" 321*760c253cSXin Li elif self.cwp_dso == "kallsyms": 322*760c253cSXin Li exact_match = "[kernel.kallsyms]" 323*760c253cSXin Li else: 324*760c253cSXin Li # This will need to be updated once there are more DSO types supported, 325*760c253cSXin Li # if user want an exact match for the field they want. 326*760c253cSXin Li exact_match = self.cwp_dso 327*760c253cSXin Li 328*760c253cSXin Li command = ( 329*760c253cSXin Li f"{perf_file} report -n -s dso -i " 330*760c253cSXin Li f"{chroot_perf_data_file} 2> /dev/null" 331*760c253cSXin Li ) 332*760c253cSXin Li _, result, _ = self.ce.ChrootRunCommandWOutput( 333*760c253cSXin Li self.chromeos_root, command 334*760c253cSXin Li ) 335*760c253cSXin Li # Accumulate the sample count for all matched fields. 336*760c253cSXin Li # Each line looks like this: 337*760c253cSXin Li # 45.42% 237210 chrome 338*760c253cSXin Li # And we want the second number which is the sample count. 339*760c253cSXin Li samples = 0 340*760c253cSXin Li try: 341*760c253cSXin Li for line in result.split("\n"): 342*760c253cSXin Li attr = line.split() 343*760c253cSXin Li if len(attr) == 3 and "%" in attr[0]: 344*760c253cSXin Li if exact_match and exact_match != attr[2]: 345*760c253cSXin Li continue 346*760c253cSXin Li samples += int(attr[1]) 347*760c253cSXin Li except: 348*760c253cSXin Li raise RuntimeError("Cannot parse perf dso result") 349*760c253cSXin Li 350*760c253cSXin Li actual_samples += samples 351*760c253cSXin Li 352*760c253cSXin Li # Remove idle cycles from the accumulated sample count. 353*760c253cSXin Li perf_report_file = f"{perf_data_file}.report" 354*760c253cSXin Li if not os.path.exists(perf_report_file): 355*760c253cSXin Li raise RuntimeError( 356*760c253cSXin Li f"Missing perf report file: {perf_report_file}" 357*760c253cSXin Li ) 358*760c253cSXin Li 359*760c253cSXin Li idle_functions = { 360*760c253cSXin Li "[kernel.kallsyms]": ( 361*760c253cSXin Li "intel_idle", 362*760c253cSXin Li "arch_cpu_idle", 363*760c253cSXin Li "intel_idle", 364*760c253cSXin Li "cpu_startup_entry", 365*760c253cSXin Li "default_idle", 366*760c253cSXin Li "cpu_idle_loop", 367*760c253cSXin Li "do_idle", 368*760c253cSXin Li "cpuidle_enter_state", 369*760c253cSXin Li ), 370*760c253cSXin Li } 371*760c253cSXin Li idle_samples = 0 372*760c253cSXin Li 373*760c253cSXin Li with open(perf_report_file, encoding="utf-8") as f: 374*760c253cSXin Li try: 375*760c253cSXin Li for line in f: 376*760c253cSXin Li line = line.strip() 377*760c253cSXin Li if not line or line[0] == "#": 378*760c253cSXin Li continue 379*760c253cSXin Li # Each line has the following fields, 380*760c253cSXin Li # pylint: disable=line-too-long 381*760c253cSXin Li # Overhead Samples Command Shared Object Symbol 382*760c253cSXin Li # pylint: disable=line-too-long 383*760c253cSXin Li # 1.48% 60 swapper [kernel.kallsyms] [k] intel_idle 384*760c253cSXin Li # pylint: disable=line-too-long 385*760c253cSXin Li # 0.00% 1 shill libshill-net.so [.] std::__1::vector<unsigned char, std::__1::allocator<unsigned char> >::vector<unsigned char const*> 386*760c253cSXin Li _, samples, _, dso, _, function = line.split(None, 5) 387*760c253cSXin Li 388*760c253cSXin Li if ( 389*760c253cSXin Li dso in idle_functions 390*760c253cSXin Li and function in idle_functions[dso] 391*760c253cSXin Li ): 392*760c253cSXin Li if self.log_level != "verbose": 393*760c253cSXin Li self._logger.LogOutput( 394*760c253cSXin Li "Removing %s samples from %s in %s" 395*760c253cSXin Li % (samples, function, dso) 396*760c253cSXin Li ) 397*760c253cSXin Li idle_samples += int(samples) 398*760c253cSXin Li except: 399*760c253cSXin Li raise RuntimeError("Cannot parse perf report") 400*760c253cSXin Li actual_samples -= idle_samples 401*760c253cSXin Li return [actual_samples, "samples"] 402*760c253cSXin Li 403*760c253cSXin Li def GetResultsDir(self): 404*760c253cSXin Li if self.suite == "tast": 405*760c253cSXin Li mo = re.search(r"Writing results to (\S+)", self.out) 406*760c253cSXin Li else: 407*760c253cSXin Li mo = re.search(r"Results placed in (\S+)", self.out) 408*760c253cSXin Li if mo: 409*760c253cSXin Li result = mo.group(1) 410*760c253cSXin Li return result 411*760c253cSXin Li raise RuntimeError("Could not find results directory.") 412*760c253cSXin Li 413*760c253cSXin Li def FindFilesInResultsDir(self, find_args): 414*760c253cSXin Li if not self.results_dir: 415*760c253cSXin Li return "" 416*760c253cSXin Li 417*760c253cSXin Li command = "find %s %s" % (self.results_dir, find_args) 418*760c253cSXin Li ret, out, _ = self.ce.RunCommandWOutput(command, print_to_console=False) 419*760c253cSXin Li if ret: 420*760c253cSXin Li raise RuntimeError("Could not run find command!") 421*760c253cSXin Li return out 422*760c253cSXin Li 423*760c253cSXin Li def GetResultsFile(self): 424*760c253cSXin Li if self.suite == "telemetry_Crosperf": 425*760c253cSXin Li return self.FindFilesInResultsDir( 426*760c253cSXin Li "-name histograms.json" 427*760c253cSXin Li ).splitlines() 428*760c253cSXin Li return self.FindFilesInResultsDir( 429*760c253cSXin Li "-name results-chart.json" 430*760c253cSXin Li ).splitlines() 431*760c253cSXin Li 432*760c253cSXin Li def GetPerfDataFiles(self): 433*760c253cSXin Li return self.FindFilesInResultsDir("-name perf.data").splitlines() 434*760c253cSXin Li 435*760c253cSXin Li def GetPerfReportFiles(self): 436*760c253cSXin Li return self.FindFilesInResultsDir("-name perf.data.report").splitlines() 437*760c253cSXin Li 438*760c253cSXin Li def GetDataMeasurementsFiles(self): 439*760c253cSXin Li result = self.FindFilesInResultsDir( 440*760c253cSXin Li "-name perf_measurements" 441*760c253cSXin Li ).splitlines() 442*760c253cSXin Li if not result: 443*760c253cSXin Li if self.suite == "telemetry_Crosperf": 444*760c253cSXin Li result = self.FindFilesInResultsDir( 445*760c253cSXin Li "-name histograms.json" 446*760c253cSXin Li ).splitlines() 447*760c253cSXin Li else: 448*760c253cSXin Li result = self.FindFilesInResultsDir( 449*760c253cSXin Li "-name results-chart.json" 450*760c253cSXin Li ).splitlines() 451*760c253cSXin Li return result 452*760c253cSXin Li 453*760c253cSXin Li def GetTurbostatFile(self): 454*760c253cSXin Li """Get turbostat log path string.""" 455*760c253cSXin Li return self.FindFilesInResultsDir("-name turbostat.log").split("\n")[0] 456*760c253cSXin Li 457*760c253cSXin Li def GetCpustatsFile(self): 458*760c253cSXin Li """Get cpustats log path string.""" 459*760c253cSXin Li return self.FindFilesInResultsDir("-name cpustats.log").split("\n")[0] 460*760c253cSXin Li 461*760c253cSXin Li def GetCpuinfoFile(self): 462*760c253cSXin Li """Get cpustats log path string.""" 463*760c253cSXin Li return self.FindFilesInResultsDir("-name cpuinfo.log").split("\n")[0] 464*760c253cSXin Li 465*760c253cSXin Li def GetTopFile(self): 466*760c253cSXin Li """Get cpustats log path string.""" 467*760c253cSXin Li return self.FindFilesInResultsDir("-name top.log").split("\n")[0] 468*760c253cSXin Li 469*760c253cSXin Li def GetWaitTimeFile(self): 470*760c253cSXin Li """Get wait time log path string.""" 471*760c253cSXin Li return self.FindFilesInResultsDir("-name wait_time.log").split("\n")[0] 472*760c253cSXin Li 473*760c253cSXin Li def _CheckDebugPath(self, option, path): 474*760c253cSXin Li out_chroot_path = misc.GetOutsideChrootPath(self.chromeos_root, path) 475*760c253cSXin Li if os.path.exists(out_chroot_path): 476*760c253cSXin Li if option == "kallsyms": 477*760c253cSXin Li path = os.path.join(path, "System.map-*") 478*760c253cSXin Li return "--" + option + " " + path 479*760c253cSXin Li else: 480*760c253cSXin Li print( 481*760c253cSXin Li "** WARNING **: --%s option not applied, %s does not exist" 482*760c253cSXin Li % (option, out_chroot_path) 483*760c253cSXin Li ) 484*760c253cSXin Li return "" 485*760c253cSXin Li 486*760c253cSXin Li def GeneratePerfReportFiles(self): 487*760c253cSXin Li perf_report_files = [] 488*760c253cSXin Li for perf_data_file in self.perf_data_files: 489*760c253cSXin Li # Generate a perf.report and store it side-by-side with the perf.data 490*760c253cSXin Li # file. 491*760c253cSXin Li chroot_perf_data_file = misc.GetInsideChrootPath( 492*760c253cSXin Li self.chromeos_root, perf_data_file 493*760c253cSXin Li ) 494*760c253cSXin Li perf_report_file = "%s.report" % perf_data_file 495*760c253cSXin Li if os.path.exists(perf_report_file): 496*760c253cSXin Li raise RuntimeError( 497*760c253cSXin Li "Perf report file already exists: %s" % perf_report_file 498*760c253cSXin Li ) 499*760c253cSXin Li chroot_perf_report_file = misc.GetInsideChrootPath( 500*760c253cSXin Li self.chromeos_root, perf_report_file 501*760c253cSXin Li ) 502*760c253cSXin Li perf_path = misc.GetOutsideChrootPath( 503*760c253cSXin Li self.chromeos_root, "/usr/bin/perf" 504*760c253cSXin Li ) 505*760c253cSXin Li 506*760c253cSXin Li perf_file = "/usr/sbin/perf" 507*760c253cSXin Li if os.path.exists(perf_path): 508*760c253cSXin Li perf_file = "/usr/bin/perf" 509*760c253cSXin Li 510*760c253cSXin Li debug_path = self.label.debug_path 511*760c253cSXin Li 512*760c253cSXin Li if debug_path: 513*760c253cSXin Li symfs = "--symfs " + debug_path 514*760c253cSXin Li vmlinux = "--vmlinux " + os.path.join( 515*760c253cSXin Li debug_path, "usr", "lib", "debug", "boot", "vmlinux" 516*760c253cSXin Li ) 517*760c253cSXin Li kallsyms = "" 518*760c253cSXin Li print( 519*760c253cSXin Li "** WARNING **: --kallsyms option not applied, no System.map-* " 520*760c253cSXin Li "for downloaded image." 521*760c253cSXin Li ) 522*760c253cSXin Li else: 523*760c253cSXin Li if self.label.image_type != "local": 524*760c253cSXin Li print( 525*760c253cSXin Li "** WARNING **: Using local debug info in /build, this may " 526*760c253cSXin Li "not match the downloaded image." 527*760c253cSXin Li ) 528*760c253cSXin Li build_path = os.path.join("/build", self.board) 529*760c253cSXin Li symfs = self._CheckDebugPath("symfs", build_path) 530*760c253cSXin Li vmlinux_path = os.path.join( 531*760c253cSXin Li build_path, "usr/lib/debug/boot/vmlinux" 532*760c253cSXin Li ) 533*760c253cSXin Li vmlinux = self._CheckDebugPath("vmlinux", vmlinux_path) 534*760c253cSXin Li kallsyms_path = os.path.join(build_path, "boot") 535*760c253cSXin Li kallsyms = self._CheckDebugPath("kallsyms", kallsyms_path) 536*760c253cSXin Li 537*760c253cSXin Li command = "%s report -n %s %s %s -i %s --stdio > %s" % ( 538*760c253cSXin Li perf_file, 539*760c253cSXin Li symfs, 540*760c253cSXin Li vmlinux, 541*760c253cSXin Li kallsyms, 542*760c253cSXin Li chroot_perf_data_file, 543*760c253cSXin Li chroot_perf_report_file, 544*760c253cSXin Li ) 545*760c253cSXin Li if self.log_level != "verbose": 546*760c253cSXin Li self._logger.LogOutput( 547*760c253cSXin Li "Generating perf report...\nCMD: %s" % command 548*760c253cSXin Li ) 549*760c253cSXin Li exit_code = self.ce.ChrootRunCommand(self.chromeos_root, command) 550*760c253cSXin Li if exit_code == 0: 551*760c253cSXin Li if self.log_level != "verbose": 552*760c253cSXin Li self._logger.LogOutput( 553*760c253cSXin Li "Perf report generated successfully." 554*760c253cSXin Li ) 555*760c253cSXin Li else: 556*760c253cSXin Li raise RuntimeError( 557*760c253cSXin Li "Perf report not generated correctly. CMD: %s" % command 558*760c253cSXin Li ) 559*760c253cSXin Li 560*760c253cSXin Li # Add a keyval to the dictionary for the events captured. 561*760c253cSXin Li perf_report_files.append( 562*760c253cSXin Li misc.GetOutsideChrootPath( 563*760c253cSXin Li self.chromeos_root, chroot_perf_report_file 564*760c253cSXin Li ) 565*760c253cSXin Li ) 566*760c253cSXin Li return perf_report_files 567*760c253cSXin Li 568*760c253cSXin Li def GatherPerfResults(self): 569*760c253cSXin Li report_id = 0 570*760c253cSXin Li for perf_report_file in self.perf_report_files: 571*760c253cSXin Li with open(perf_report_file, "r", encoding="utf-8") as f: 572*760c253cSXin Li report_contents = f.read() 573*760c253cSXin Li for group in re.findall( 574*760c253cSXin Li r"Events: (\S+) (\S+)", report_contents 575*760c253cSXin Li ): 576*760c253cSXin Li num_events = group[0] 577*760c253cSXin Li event_name = group[1] 578*760c253cSXin Li key = "perf_%s_%s" % (report_id, event_name) 579*760c253cSXin Li value = str(misc.UnitToNumber(num_events)) 580*760c253cSXin Li self.keyvals[key] = value 581*760c253cSXin Li 582*760c253cSXin Li def PopulateFromRun(self, out, err, retval, test, suite, cwp_dso): 583*760c253cSXin Li self.board = self.label.board 584*760c253cSXin Li self.out = out 585*760c253cSXin Li self.err = err 586*760c253cSXin Li self.retval = retval 587*760c253cSXin Li self.test_name = test 588*760c253cSXin Li self.suite = suite 589*760c253cSXin Li self.cwp_dso = cwp_dso 590*760c253cSXin Li self.chroot_results_dir = self.GetResultsDir() 591*760c253cSXin Li self.results_dir = misc.GetOutsideChrootPath( 592*760c253cSXin Li self.chromeos_root, self.chroot_results_dir 593*760c253cSXin Li ) 594*760c253cSXin Li self.results_file = self.GetResultsFile() 595*760c253cSXin Li self.perf_data_files = self.GetPerfDataFiles() 596*760c253cSXin Li # Include all perf.report data in table. 597*760c253cSXin Li self.perf_report_files = self.GeneratePerfReportFiles() 598*760c253cSXin Li self.turbostat_log_file = self.GetTurbostatFile() 599*760c253cSXin Li self.cpustats_log_file = self.GetCpustatsFile() 600*760c253cSXin Li self.cpuinfo_file = self.GetCpuinfoFile() 601*760c253cSXin Li self.top_log_file = self.GetTopFile() 602*760c253cSXin Li self.wait_time_log_file = self.GetWaitTimeFile() 603*760c253cSXin Li # TODO(asharif): Do something similar with perf stat. 604*760c253cSXin Li 605*760c253cSXin Li # Grab keyvals from the directory. 606*760c253cSXin Li self.ProcessResults() 607*760c253cSXin Li 608*760c253cSXin Li def ProcessChartResults(self): 609*760c253cSXin Li # Open and parse the json results file generated by telemetry/test_that. 610*760c253cSXin Li if not self.results_file: 611*760c253cSXin Li raise IOError("No results file found.") 612*760c253cSXin Li filename = self.results_file[0] 613*760c253cSXin Li if not filename.endswith(".json"): 614*760c253cSXin Li raise IOError( 615*760c253cSXin Li "Attempt to call json on non-json file: %s" % filename 616*760c253cSXin Li ) 617*760c253cSXin Li if not os.path.exists(filename): 618*760c253cSXin Li raise IOError("%s does not exist" % filename) 619*760c253cSXin Li 620*760c253cSXin Li keyvals = {} 621*760c253cSXin Li with open(filename, "r", encoding="utf-8") as f: 622*760c253cSXin Li raw_dict = json.load(f) 623*760c253cSXin Li if "charts" in raw_dict: 624*760c253cSXin Li raw_dict = raw_dict["charts"] 625*760c253cSXin Li for k, field_dict in raw_dict.items(): 626*760c253cSXin Li for item in field_dict: 627*760c253cSXin Li keyname = k + "__" + item 628*760c253cSXin Li value_dict = field_dict[item] 629*760c253cSXin Li if "value" in value_dict: 630*760c253cSXin Li result = value_dict["value"] 631*760c253cSXin Li elif "values" in value_dict: 632*760c253cSXin Li values = value_dict["values"] 633*760c253cSXin Li if not values: 634*760c253cSXin Li continue 635*760c253cSXin Li if ( 636*760c253cSXin Li "type" in value_dict 637*760c253cSXin Li and value_dict["type"] == "list_of_scalar_values" 638*760c253cSXin Li and values != "null" 639*760c253cSXin Li ): 640*760c253cSXin Li result = sum(values) / float(len(values)) 641*760c253cSXin Li else: 642*760c253cSXin Li result = values 643*760c253cSXin Li else: 644*760c253cSXin Li continue 645*760c253cSXin Li units = value_dict["units"] 646*760c253cSXin Li new_value = [result, units] 647*760c253cSXin Li keyvals[keyname] = new_value 648*760c253cSXin Li return keyvals 649*760c253cSXin Li 650*760c253cSXin Li def ProcessTurbostatResults(self): 651*760c253cSXin Li """Given turbostat_log_file non-null parse cpu stats from file. 652*760c253cSXin Li 653*760c253cSXin Li Returns: 654*760c253cSXin Li Dictionary of 'cpufreq', 'cputemp' where each 655*760c253cSXin Li includes dictionary 'all': [list_of_values] 656*760c253cSXin Li 657*760c253cSXin Li Example of the output of turbostat_log. 658*760c253cSXin Li ---------------------- 659*760c253cSXin Li CPU Avg_MHz Busy% Bzy_MHz TSC_MHz IRQ CoreTmp 660*760c253cSXin Li - 329 12.13 2723 2393 10975 77 661*760c253cSXin Li 0 336 12.41 2715 2393 6328 77 662*760c253cSXin Li 2 323 11.86 2731 2393 4647 69 663*760c253cSXin Li CPU Avg_MHz Busy% Bzy_MHz TSC_MHz IRQ CoreTmp 664*760c253cSXin Li - 1940 67.46 2884 2393 39920 83 665*760c253cSXin Li 0 1827 63.70 2877 2393 21184 83 666*760c253cSXin Li """ 667*760c253cSXin Li cpustats = {} 668*760c253cSXin Li read_data = "" 669*760c253cSXin Li with open(self.turbostat_log_file, encoding="utf-8") as f: 670*760c253cSXin Li read_data = f.readlines() 671*760c253cSXin Li 672*760c253cSXin Li if not read_data: 673*760c253cSXin Li self._logger.LogOutput("WARNING: Turbostat output file is empty.") 674*760c253cSXin Li return {} 675*760c253cSXin Li 676*760c253cSXin Li # First line always contains the header. 677*760c253cSXin Li stats = read_data[0].split() 678*760c253cSXin Li 679*760c253cSXin Li # Mandatory parameters. 680*760c253cSXin Li if "CPU" not in stats: 681*760c253cSXin Li self._logger.LogOutput( 682*760c253cSXin Li "WARNING: Missing data for CPU# in Turbostat output." 683*760c253cSXin Li ) 684*760c253cSXin Li return {} 685*760c253cSXin Li if "Bzy_MHz" not in stats: 686*760c253cSXin Li self._logger.LogOutput( 687*760c253cSXin Li "WARNING: Missing data for Bzy_MHz in Turbostat output." 688*760c253cSXin Li ) 689*760c253cSXin Li return {} 690*760c253cSXin Li cpu_index = stats.index("CPU") 691*760c253cSXin Li cpufreq_index = stats.index("Bzy_MHz") 692*760c253cSXin Li cpufreq = cpustats.setdefault("cpufreq", {"all": []}) 693*760c253cSXin Li 694*760c253cSXin Li # Optional parameters. 695*760c253cSXin Li cputemp_index = -1 696*760c253cSXin Li if "CoreTmp" in stats: 697*760c253cSXin Li cputemp_index = stats.index("CoreTmp") 698*760c253cSXin Li cputemp = cpustats.setdefault("cputemp", {"all": []}) 699*760c253cSXin Li 700*760c253cSXin Li # Parse data starting from the second line ignoring repeating headers. 701*760c253cSXin Li for st in read_data[1:]: 702*760c253cSXin Li # Data represented by int or float separated by spaces. 703*760c253cSXin Li numbers = st.split() 704*760c253cSXin Li if not all( 705*760c253cSXin Li word.replace(".", "", 1).isdigit() for word in numbers[1:] 706*760c253cSXin Li ): 707*760c253cSXin Li # Skip the line if data mismatch. 708*760c253cSXin Li continue 709*760c253cSXin Li if numbers[cpu_index] != "-": 710*760c253cSXin Li # Ignore Core-specific statistics which starts with Core number. 711*760c253cSXin Li # Combined statistics for all core has "-" CPU identifier. 712*760c253cSXin Li continue 713*760c253cSXin Li 714*760c253cSXin Li cpufreq["all"].append(int(numbers[cpufreq_index])) 715*760c253cSXin Li if cputemp_index != -1: 716*760c253cSXin Li cputemp["all"].append(int(numbers[cputemp_index])) 717*760c253cSXin Li return cpustats 718*760c253cSXin Li 719*760c253cSXin Li def ProcessTopResults(self): 720*760c253cSXin Li """Given self.top_log_file process top log data. 721*760c253cSXin Li 722*760c253cSXin Li Returns: 723*760c253cSXin Li List of dictionaries with the following keyvals: 724*760c253cSXin Li 'cmd': command name (string), 725*760c253cSXin Li 'cpu_use_avg': average cpu usage (float), 726*760c253cSXin Li 'count': number of occurrences (int), 727*760c253cSXin Li 'top5_cpu_use': up to 5 highest cpu usages (descending list of floats) 728*760c253cSXin Li 729*760c253cSXin Li Example of the top log: 730*760c253cSXin Li PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 731*760c253cSXin Li 4102 chronos 12 -8 3454472 238300 118188 R 41.8 6.1 0:08.37 chrome 732*760c253cSXin Li 375 root 0 -20 0 0 0 S 5.9 0.0 0:00.17 kworker 733*760c253cSXin Li 617 syslog 20 0 25332 8372 7888 S 5.9 0.2 0:00.77 systemd 734*760c253cSXin Li 735*760c253cSXin Li PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 736*760c253cSXin Li 5745 chronos 20 0 5438580 139328 67988 R 122.8 3.6 0:04.26 chrome 737*760c253cSXin Li 912 root -51 0 0 0 0 S 2.0 0.0 0:01.04 irq/cro 738*760c253cSXin Li 121 root 20 0 0 0 0 S 1.0 0.0 0:00.45 spi5 739*760c253cSXin Li """ 740*760c253cSXin Li all_data = "" 741*760c253cSXin Li with open(self.top_log_file, encoding="utf-8") as f: 742*760c253cSXin Li all_data = f.read() 743*760c253cSXin Li 744*760c253cSXin Li if not all_data: 745*760c253cSXin Li self._logger.LogOutput("WARNING: Top log file is empty.") 746*760c253cSXin Li return [] 747*760c253cSXin Li 748*760c253cSXin Li top_line_regex = re.compile( 749*760c253cSXin Li r""" 750*760c253cSXin Li ^\s*(?P<pid>\d+)\s+ # Group 1: PID 751*760c253cSXin Li \S+\s+\S+\s+-?\d+\s+ # Ignore: user, prio, nice 752*760c253cSXin Li \d+\s+\d+\s+\d+\s+ # Ignore: virt/res/shared mem 753*760c253cSXin Li \S+\s+ # Ignore: state 754*760c253cSXin Li (?P<cpu_use>\d+\.\d+)\s+ # Group 2: CPU usage 755*760c253cSXin Li \d+\.\d+\s+\d+:\d+\.\d+\s+ # Ignore: mem usage, time 756*760c253cSXin Li (?P<cmd>\S+)$ # Group 3: command 757*760c253cSXin Li """, 758*760c253cSXin Li re.VERBOSE, 759*760c253cSXin Li ) 760*760c253cSXin Li # Page represents top log data per one measurement within time interval 761*760c253cSXin Li # 'top_interval'. 762*760c253cSXin Li # Pages separated by empty line. 763*760c253cSXin Li pages = all_data.split("\n\n") 764*760c253cSXin Li # Snapshots are structured representation of the pages. 765*760c253cSXin Li snapshots = [] 766*760c253cSXin Li for page in pages: 767*760c253cSXin Li if not page: 768*760c253cSXin Li continue 769*760c253cSXin Li 770*760c253cSXin Li # Snapshot list will contain all processes (command duplicates are 771*760c253cSXin Li # allowed). 772*760c253cSXin Li snapshot = [] 773*760c253cSXin Li for line in page.splitlines(): 774*760c253cSXin Li match = top_line_regex.match(line) 775*760c253cSXin Li if match: 776*760c253cSXin Li # Top line is valid, collect data. 777*760c253cSXin Li process = { 778*760c253cSXin Li # NOTE: One command may be represented by multiple processes. 779*760c253cSXin Li "cmd": match.group("cmd"), 780*760c253cSXin Li "pid": match.group("pid"), 781*760c253cSXin Li "cpu_use": float(match.group("cpu_use")), 782*760c253cSXin Li } 783*760c253cSXin Li 784*760c253cSXin Li # Filter out processes with 0 CPU usage and top command. 785*760c253cSXin Li if process["cpu_use"] > 0 and process["cmd"] != "top": 786*760c253cSXin Li snapshot.append(process) 787*760c253cSXin Li 788*760c253cSXin Li # If page contained meaningful data add snapshot to the list. 789*760c253cSXin Li if snapshot: 790*760c253cSXin Li snapshots.append(snapshot) 791*760c253cSXin Li 792*760c253cSXin Li # Define threshold of CPU usage when Chrome is busy, i.e. benchmark is 793*760c253cSXin Li # running. 794*760c253cSXin Li # Ideally it should be 100% but it will be hardly reachable with 1 core. 795*760c253cSXin Li # Statistics on DUT with 2-6 cores shows that chrome load of 100%, 95% and 796*760c253cSXin Li # 90% equally occurs in 72-74% of all top log snapshots. 797*760c253cSXin Li # Further decreasing of load threshold leads to a shifting percent of 798*760c253cSXin Li # "high load" snapshots which might include snapshots when benchmark is 799*760c253cSXin Li # not running. 800*760c253cSXin Li # On 1-core DUT 90% chrome cpu load occurs in 55%, 95% in 33% and 100% in 2% 801*760c253cSXin Li # of snapshots accordingly. 802*760c253cSXin Li # Threshold of "high load" is reduced to 70% (from 90) when we switched to 803*760c253cSXin Li # topstats per process. From experiment data the rest 20% are distributed 804*760c253cSXin Li # among other chrome processes. 805*760c253cSXin Li CHROME_HIGH_CPU_LOAD = 70 806*760c253cSXin Li # Number of snapshots where chrome is heavily used. 807*760c253cSXin Li high_load_snapshots = 0 808*760c253cSXin Li # Total CPU use per process in ALL active snapshots. 809*760c253cSXin Li cmd_total_cpu_use = collections.defaultdict(float) 810*760c253cSXin Li # Top CPU usages per command. 811*760c253cSXin Li cmd_top5_cpu_use = collections.defaultdict(list) 812*760c253cSXin Li # List of Top Commands to be returned. 813*760c253cSXin Li topcmds = [] 814*760c253cSXin Li 815*760c253cSXin Li for snapshot_processes in snapshots: 816*760c253cSXin Li # CPU usage per command, per PID in one snapshot. 817*760c253cSXin Li cmd_cpu_use_per_snapshot = collections.defaultdict(dict) 818*760c253cSXin Li for process in snapshot_processes: 819*760c253cSXin Li cmd = process["cmd"] 820*760c253cSXin Li cpu_use = process["cpu_use"] 821*760c253cSXin Li pid = process["pid"] 822*760c253cSXin Li cmd_cpu_use_per_snapshot[cmd][pid] = cpu_use 823*760c253cSXin Li 824*760c253cSXin Li # Chrome processes, pid: cpu_usage. 825*760c253cSXin Li chrome_processes = cmd_cpu_use_per_snapshot.get("chrome", {}) 826*760c253cSXin Li chrome_cpu_use_list = chrome_processes.values() 827*760c253cSXin Li 828*760c253cSXin Li if ( 829*760c253cSXin Li chrome_cpu_use_list 830*760c253cSXin Li and max(chrome_cpu_use_list) > CHROME_HIGH_CPU_LOAD 831*760c253cSXin Li ): 832*760c253cSXin Li # CPU usage of any of the "chrome" processes exceeds "High load" 833*760c253cSXin Li # threshold which means DUT is busy running a benchmark. 834*760c253cSXin Li high_load_snapshots += 1 835*760c253cSXin Li for cmd, cpu_use_per_pid in cmd_cpu_use_per_snapshot.items(): 836*760c253cSXin Li for pid, cpu_use in cpu_use_per_pid.items(): 837*760c253cSXin Li # Append PID to the name of the command. 838*760c253cSXin Li cmd_with_pid = cmd + "-" + pid 839*760c253cSXin Li cmd_total_cpu_use[cmd_with_pid] += cpu_use 840*760c253cSXin Li 841*760c253cSXin Li # Add cpu_use into command top cpu usages, sorted in descending 842*760c253cSXin Li # order. 843*760c253cSXin Li heapq.heappush( 844*760c253cSXin Li cmd_top5_cpu_use[cmd_with_pid], round(cpu_use, 1) 845*760c253cSXin Li ) 846*760c253cSXin Li 847*760c253cSXin Li for consumer, usage in sorted( 848*760c253cSXin Li cmd_total_cpu_use.items(), key=lambda x: x[1], reverse=True 849*760c253cSXin Li ): 850*760c253cSXin Li # Iterate through commands by descending order of total CPU usage. 851*760c253cSXin Li topcmd = { 852*760c253cSXin Li "cmd": consumer, 853*760c253cSXin Li "cpu_use_avg": usage / high_load_snapshots, 854*760c253cSXin Li "count": len(cmd_top5_cpu_use[consumer]), 855*760c253cSXin Li "top5_cpu_use": heapq.nlargest(5, cmd_top5_cpu_use[consumer]), 856*760c253cSXin Li } 857*760c253cSXin Li topcmds.append(topcmd) 858*760c253cSXin Li 859*760c253cSXin Li return topcmds 860*760c253cSXin Li 861*760c253cSXin Li def ProcessCpustatsResults(self): 862*760c253cSXin Li """Given cpustats_log_file non-null parse cpu data from file. 863*760c253cSXin Li 864*760c253cSXin Li Returns: 865*760c253cSXin Li Dictionary of 'cpufreq', 'cputemp' where each 866*760c253cSXin Li includes dictionary of parameter: [list_of_values] 867*760c253cSXin Li 868*760c253cSXin Li Example of cpustats.log output. 869*760c253cSXin Li ---------------------- 870*760c253cSXin Li /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq 1512000 871*760c253cSXin Li /sys/devices/system/cpu/cpu2/cpufreq/cpuinfo_cur_freq 2016000 872*760c253cSXin Li little-cpu 41234 873*760c253cSXin Li big-cpu 51234 874*760c253cSXin Li 875*760c253cSXin Li If cores share the same policy their frequencies may always match 876*760c253cSXin Li on some devices. 877*760c253cSXin Li To make report concise we should eliminate redundancy in the output. 878*760c253cSXin Li Function removes cpuN data if it duplicates data from other cores. 879*760c253cSXin Li """ 880*760c253cSXin Li 881*760c253cSXin Li cpustats = {} 882*760c253cSXin Li read_data = "" 883*760c253cSXin Li with open(self.cpustats_log_file, encoding="utf-8") as f: 884*760c253cSXin Li read_data = f.readlines() 885*760c253cSXin Li 886*760c253cSXin Li if not read_data: 887*760c253cSXin Li self._logger.LogOutput("WARNING: Cpustats output file is empty.") 888*760c253cSXin Li return {} 889*760c253cSXin Li 890*760c253cSXin Li cpufreq_regex = re.compile(r"^[/\S]+/(cpu\d+)/[/\S]+\s+(\d+)$") 891*760c253cSXin Li cputemp_regex = re.compile(r"^([^/\s]+)\s+(\d+)$") 892*760c253cSXin Li 893*760c253cSXin Li for st in read_data: 894*760c253cSXin Li match = cpufreq_regex.match(st) 895*760c253cSXin Li if match: 896*760c253cSXin Li cpu = match.group(1) 897*760c253cSXin Li # CPU frequency comes in kHz. 898*760c253cSXin Li freq_khz = int(match.group(2)) 899*760c253cSXin Li freq_mhz = freq_khz / 1000 900*760c253cSXin Li # cpufreq represents a dictionary with CPU frequency-related 901*760c253cSXin Li # data from cpustats.log. 902*760c253cSXin Li cpufreq = cpustats.setdefault("cpufreq", {}) 903*760c253cSXin Li cpu_n_freq = cpufreq.setdefault(cpu, []) 904*760c253cSXin Li cpu_n_freq.append(freq_mhz) 905*760c253cSXin Li else: 906*760c253cSXin Li match = cputemp_regex.match(st) 907*760c253cSXin Li if match: 908*760c253cSXin Li therm_type = match.group(1) 909*760c253cSXin Li # The value is int, uCelsius unit. 910*760c253cSXin Li temp_uc = float(match.group(2)) 911*760c253cSXin Li # Round to XX.X float. 912*760c253cSXin Li temp_c = round(temp_uc / 1000, 1) 913*760c253cSXin Li # cputemp represents a dictionary with temperature measurements 914*760c253cSXin Li # from cpustats.log. 915*760c253cSXin Li cputemp = cpustats.setdefault("cputemp", {}) 916*760c253cSXin Li therm_type = cputemp.setdefault(therm_type, []) 917*760c253cSXin Li therm_type.append(temp_c) 918*760c253cSXin Li 919*760c253cSXin Li # Remove duplicate statistics from cpustats. 920*760c253cSXin Li pruned_stats = {} 921*760c253cSXin Li for cpukey, cpuparam in cpustats.items(): 922*760c253cSXin Li # Copy 'cpufreq' and 'cputemp'. 923*760c253cSXin Li pruned_params = pruned_stats.setdefault(cpukey, {}) 924*760c253cSXin Li for paramkey, paramvalue in sorted(cpuparam.items()): 925*760c253cSXin Li # paramvalue is list of all measured data. 926*760c253cSXin Li if paramvalue not in pruned_params.values(): 927*760c253cSXin Li pruned_params[paramkey] = paramvalue 928*760c253cSXin Li 929*760c253cSXin Li return pruned_stats 930*760c253cSXin Li 931*760c253cSXin Li def ProcessHistogramsResults(self): 932*760c253cSXin Li # Open and parse the json results file generated by telemetry/test_that. 933*760c253cSXin Li if not self.results_file: 934*760c253cSXin Li raise IOError("No results file found.") 935*760c253cSXin Li filename = self.results_file[0] 936*760c253cSXin Li if not filename.endswith(".json"): 937*760c253cSXin Li raise IOError( 938*760c253cSXin Li "Attempt to call json on non-json file: %s" % filename 939*760c253cSXin Li ) 940*760c253cSXin Li if not os.path.exists(filename): 941*760c253cSXin Li raise IOError("%s does not exist" % filename) 942*760c253cSXin Li 943*760c253cSXin Li keyvals = {} 944*760c253cSXin Li with open(filename, encoding="utf-8") as f: 945*760c253cSXin Li histograms = json.load(f) 946*760c253cSXin Li value_map = {} 947*760c253cSXin Li # Gets generic set values. 948*760c253cSXin Li for obj in histograms: 949*760c253cSXin Li if "type" in obj and obj["type"] == "GenericSet": 950*760c253cSXin Li value_map[obj["guid"]] = obj["values"] 951*760c253cSXin Li 952*760c253cSXin Li for obj in histograms: 953*760c253cSXin Li if "name" not in obj or "sampleValues" not in obj: 954*760c253cSXin Li continue 955*760c253cSXin Li metric_name = obj["name"] 956*760c253cSXin Li vals = obj["sampleValues"] 957*760c253cSXin Li if isinstance(vals, list): 958*760c253cSXin Li # Remove None elements from the list 959*760c253cSXin Li vals = [val for val in vals if val is not None] 960*760c253cSXin Li if vals: 961*760c253cSXin Li result = float(sum(vals)) / len(vals) 962*760c253cSXin Li else: 963*760c253cSXin Li result = 0 964*760c253cSXin Li else: 965*760c253cSXin Li result = vals 966*760c253cSXin Li unit = obj["unit"] 967*760c253cSXin Li diagnostics = obj["diagnostics"] 968*760c253cSXin Li # for summaries of benchmarks 969*760c253cSXin Li key = metric_name 970*760c253cSXin Li if key not in keyvals: 971*760c253cSXin Li keyvals[key] = [[result], unit] 972*760c253cSXin Li else: 973*760c253cSXin Li keyvals[key][0].append(result) 974*760c253cSXin Li # TODO: do we need summaries of stories? 975*760c253cSXin Li # for summaries of story tags 976*760c253cSXin Li if "storyTags" in diagnostics: 977*760c253cSXin Li guid = diagnostics["storyTags"] 978*760c253cSXin Li if guid not in value_map: 979*760c253cSXin Li raise RuntimeError( 980*760c253cSXin Li "Unrecognized storyTags in %s " % (obj) 981*760c253cSXin Li ) 982*760c253cSXin Li for story_tag in value_map[guid]: 983*760c253cSXin Li key = metric_name + "__" + story_tag 984*760c253cSXin Li if key not in keyvals: 985*760c253cSXin Li keyvals[key] = [[result], unit] 986*760c253cSXin Li else: 987*760c253cSXin Li keyvals[key][0].append(result) 988*760c253cSXin Li # calculate summary 989*760c253cSXin Li for key in keyvals: 990*760c253cSXin Li vals = keyvals[key][0] 991*760c253cSXin Li unit = keyvals[key][1] 992*760c253cSXin Li result = float(sum(vals)) / len(vals) 993*760c253cSXin Li keyvals[key] = [result, unit] 994*760c253cSXin Li return keyvals 995*760c253cSXin Li 996*760c253cSXin Li def ReadPidFromPerfData(self): 997*760c253cSXin Li """Read PIDs from perf.data files. 998*760c253cSXin Li 999*760c253cSXin Li Extract PID from perf.data if "perf record" was running per process, 1000*760c253cSXin Li i.e. with "-p <PID>" and no "-a". 1001*760c253cSXin Li 1002*760c253cSXin Li Returns: 1003*760c253cSXin Li pids: list of PIDs. 1004*760c253cSXin Li 1005*760c253cSXin Li Raises: 1006*760c253cSXin Li PerfDataReadError when perf.data header reading fails. 1007*760c253cSXin Li """ 1008*760c253cSXin Li cmd = ["/usr/bin/perf", "report", "--header-only", "-i"] 1009*760c253cSXin Li pids = [] 1010*760c253cSXin Li 1011*760c253cSXin Li for perf_data_path in self.perf_data_files: 1012*760c253cSXin Li perf_data_path_in_chroot = misc.GetInsideChrootPath( 1013*760c253cSXin Li self.chromeos_root, perf_data_path 1014*760c253cSXin Li ) 1015*760c253cSXin Li path_str = " ".join(cmd + [perf_data_path_in_chroot]) 1016*760c253cSXin Li status, output, _ = self.ce.ChrootRunCommandWOutput( 1017*760c253cSXin Li self.chromeos_root, path_str 1018*760c253cSXin Li ) 1019*760c253cSXin Li if status: 1020*760c253cSXin Li # Error of reading a perf.data profile is fatal. 1021*760c253cSXin Li raise PerfDataReadError( 1022*760c253cSXin Li f"Failed to read perf.data profile: {path_str}" 1023*760c253cSXin Li ) 1024*760c253cSXin Li 1025*760c253cSXin Li # Pattern to search a line with "perf record" command line: 1026*760c253cSXin Li # # cmdline : /usr/bin/perf record -e instructions -p 123" 1027*760c253cSXin Li cmdline_regex = re.compile( 1028*760c253cSXin Li r"^\#\scmdline\s:\s+(?P<cmd>.*perf\s+record\s+.*)$" 1029*760c253cSXin Li ) 1030*760c253cSXin Li # Pattern to search PID in a command line. 1031*760c253cSXin Li pid_regex = re.compile(r"^.*\s-p\s(?P<pid>\d+)\s*.*$") 1032*760c253cSXin Li for line in output.splitlines(): 1033*760c253cSXin Li cmd_match = cmdline_regex.match(line) 1034*760c253cSXin Li if cmd_match: 1035*760c253cSXin Li # Found a perf command line. 1036*760c253cSXin Li cmdline = cmd_match.group("cmd") 1037*760c253cSXin Li # '-a' is a system-wide mode argument. 1038*760c253cSXin Li if "-a" not in cmdline.split(): 1039*760c253cSXin Li # It can be that perf was attached to PID and was still running in 1040*760c253cSXin Li # system-wide mode. 1041*760c253cSXin Li # We filter out this case here since it's not per-process. 1042*760c253cSXin Li pid_match = pid_regex.match(cmdline) 1043*760c253cSXin Li if pid_match: 1044*760c253cSXin Li pids.append(pid_match.group("pid")) 1045*760c253cSXin Li # Stop the search and move to the next perf.data file. 1046*760c253cSXin Li break 1047*760c253cSXin Li else: 1048*760c253cSXin Li # cmdline wasn't found in the header. It's a fatal error. 1049*760c253cSXin Li raise PerfDataReadError( 1050*760c253cSXin Li f"Perf command line is not found in {path_str}" 1051*760c253cSXin Li ) 1052*760c253cSXin Li return pids 1053*760c253cSXin Li 1054*760c253cSXin Li def VerifyPerfDataPID(self): 1055*760c253cSXin Li """Verify PIDs in per-process perf.data profiles. 1056*760c253cSXin Li 1057*760c253cSXin Li Check that at list one top process is profiled if perf was running in 1058*760c253cSXin Li per-process mode. 1059*760c253cSXin Li 1060*760c253cSXin Li Raises: 1061*760c253cSXin Li PidVerificationError if PID verification of per-process perf.data profiles 1062*760c253cSXin Li fail. 1063*760c253cSXin Li """ 1064*760c253cSXin Li perf_data_pids = self.ReadPidFromPerfData() 1065*760c253cSXin Li if not perf_data_pids: 1066*760c253cSXin Li # In system-wide mode there are no PIDs. 1067*760c253cSXin Li self._logger.LogOutput("System-wide perf mode. Skip verification.") 1068*760c253cSXin Li return 1069*760c253cSXin Li 1070*760c253cSXin Li # PIDs will be present only in per-process profiles. 1071*760c253cSXin Li # In this case we need to verify that profiles are collected on the 1072*760c253cSXin Li # hottest processes. 1073*760c253cSXin Li top_processes = [top_cmd["cmd"] for top_cmd in self.top_cmds] 1074*760c253cSXin Li # top_process structure: <cmd>-<pid> 1075*760c253cSXin Li top_pids = [top_process.split("-")[-1] for top_process in top_processes] 1076*760c253cSXin Li for top_pid in top_pids: 1077*760c253cSXin Li if top_pid in perf_data_pids: 1078*760c253cSXin Li self._logger.LogOutput( 1079*760c253cSXin Li "PID verification passed! " 1080*760c253cSXin Li f"Top process {top_pid} is profiled." 1081*760c253cSXin Li ) 1082*760c253cSXin Li return 1083*760c253cSXin Li raise PidVerificationError( 1084*760c253cSXin Li f"top processes {top_processes} are missing in perf.data traces with" 1085*760c253cSXin Li f" PID: {perf_data_pids}." 1086*760c253cSXin Li ) 1087*760c253cSXin Li 1088*760c253cSXin Li def ProcessResults(self, use_cache=False): 1089*760c253cSXin Li # Note that this function doesn't know anything about whether there is a 1090*760c253cSXin Li # cache hit or miss. It should process results agnostic of the cache hit 1091*760c253cSXin Li # state. 1092*760c253cSXin Li if ( 1093*760c253cSXin Li self.results_file 1094*760c253cSXin Li and self.suite == "telemetry_Crosperf" 1095*760c253cSXin Li and "histograms.json" in self.results_file[0] 1096*760c253cSXin Li ): 1097*760c253cSXin Li self.keyvals = self.ProcessHistogramsResults() 1098*760c253cSXin Li elif ( 1099*760c253cSXin Li self.results_file 1100*760c253cSXin Li and self.suite != "telemetry_Crosperf" 1101*760c253cSXin Li and "results-chart.json" in self.results_file[0] 1102*760c253cSXin Li ): 1103*760c253cSXin Li self.keyvals = self.ProcessChartResults() 1104*760c253cSXin Li else: 1105*760c253cSXin Li if not use_cache: 1106*760c253cSXin Li print( 1107*760c253cSXin Li "\n ** WARNING **: Had to use deprecated output-method to " 1108*760c253cSXin Li "collect results.\n" 1109*760c253cSXin Li ) 1110*760c253cSXin Li self.keyvals = self.GetKeyvals() 1111*760c253cSXin Li self.keyvals["retval"] = self.retval 1112*760c253cSXin Li # If we are in CWP approximation mode, we want to collect DSO samples 1113*760c253cSXin Li # for each perf.data file 1114*760c253cSXin Li if self.cwp_dso and self.retval == 0: 1115*760c253cSXin Li self.keyvals["samples"] = self.GetSamples() 1116*760c253cSXin Li # If the samples count collected from perf file is 0, we will treat 1117*760c253cSXin Li # it as a failed run. 1118*760c253cSXin Li if self.keyvals["samples"][0] == 0: 1119*760c253cSXin Li del self.keyvals["samples"] 1120*760c253cSXin Li self.keyvals["retval"] = 1 1121*760c253cSXin Li # Generate report from all perf.data files. 1122*760c253cSXin Li # Now parse all perf report files and include them in keyvals. 1123*760c253cSXin Li self.GatherPerfResults() 1124*760c253cSXin Li 1125*760c253cSXin Li cpustats = {} 1126*760c253cSXin Li # Turbostat output has higher priority of processing. 1127*760c253cSXin Li if self.turbostat_log_file: 1128*760c253cSXin Li cpustats = self.ProcessTurbostatResults() 1129*760c253cSXin Li # Process cpustats output only if turbostat has no data. 1130*760c253cSXin Li if not cpustats and self.cpustats_log_file: 1131*760c253cSXin Li cpustats = self.ProcessCpustatsResults() 1132*760c253cSXin Li if self.top_log_file: 1133*760c253cSXin Li self.top_cmds = self.ProcessTopResults() 1134*760c253cSXin Li # Verify that PID in non system-wide perf.data and top_cmds are matching. 1135*760c253cSXin Li if self.perf_data_files and self.top_cmds: 1136*760c253cSXin Li self.VerifyPerfDataPID() 1137*760c253cSXin Li if self.wait_time_log_file: 1138*760c253cSXin Li with open(self.wait_time_log_file, encoding="utf-8") as f: 1139*760c253cSXin Li wait_time = f.readline().strip() 1140*760c253cSXin Li try: 1141*760c253cSXin Li wait_time = float(wait_time) 1142*760c253cSXin Li except ValueError: 1143*760c253cSXin Li raise ValueError("Wait time in log file is not a number.") 1144*760c253cSXin Li # This is for accumulating wait time for telemtry_Crosperf runs only, 1145*760c253cSXin Li # for test_that runs, please refer to suite_runner. 1146*760c253cSXin Li self.machine.AddCooldownWaitTime(wait_time) 1147*760c253cSXin Li 1148*760c253cSXin Li for param_key, param in cpustats.items(): 1149*760c253cSXin Li for param_type, param_values in param.items(): 1150*760c253cSXin Li val_avg = sum(param_values) / len(param_values) 1151*760c253cSXin Li val_min = min(param_values) 1152*760c253cSXin Li val_max = max(param_values) 1153*760c253cSXin Li # Average data is always included. 1154*760c253cSXin Li self.keyvals["_".join([param_key, param_type, "avg"])] = val_avg 1155*760c253cSXin Li # Insert min/max results only if they deviate 1156*760c253cSXin Li # from average. 1157*760c253cSXin Li if val_min != val_avg: 1158*760c253cSXin Li self.keyvals[ 1159*760c253cSXin Li "_".join([param_key, param_type, "min"]) 1160*760c253cSXin Li ] = val_min 1161*760c253cSXin Li if val_max != val_avg: 1162*760c253cSXin Li self.keyvals[ 1163*760c253cSXin Li "_".join([param_key, param_type, "max"]) 1164*760c253cSXin Li ] = val_max 1165*760c253cSXin Li 1166*760c253cSXin Li def GetChromeVersionFromCache(self, cache_dir): 1167*760c253cSXin Li # Read chrome_version from keys file, if present. 1168*760c253cSXin Li chrome_version = "" 1169*760c253cSXin Li keys_file = os.path.join(cache_dir, CACHE_KEYS_FILE) 1170*760c253cSXin Li if os.path.exists(keys_file): 1171*760c253cSXin Li with open(keys_file, "r", encoding="utf-8") as f: 1172*760c253cSXin Li lines = f.readlines() 1173*760c253cSXin Li for l in lines: 1174*760c253cSXin Li if l.startswith("Google Chrome "): 1175*760c253cSXin Li chrome_version = l 1176*760c253cSXin Li if chrome_version.endswith("\n"): 1177*760c253cSXin Li chrome_version = chrome_version[:-1] 1178*760c253cSXin Li break 1179*760c253cSXin Li return chrome_version 1180*760c253cSXin Li 1181*760c253cSXin Li def PopulateFromCacheDir(self, cache_dir, test, suite, cwp_dso): 1182*760c253cSXin Li self.test_name = test 1183*760c253cSXin Li self.suite = suite 1184*760c253cSXin Li self.cwp_dso = cwp_dso 1185*760c253cSXin Li # Read in everything from the cache directory. 1186*760c253cSXin Li with open(os.path.join(cache_dir, RESULTS_FILE), "rb") as f: 1187*760c253cSXin Li self.out = pickle.load(f) 1188*760c253cSXin Li self.err = pickle.load(f) 1189*760c253cSXin Li self.retval = pickle.load(f) 1190*760c253cSXin Li 1191*760c253cSXin Li # Untar the tarball to a temporary directory 1192*760c253cSXin Li self.temp_dir = tempfile.mkdtemp( 1193*760c253cSXin Li dir=misc.GetOutsideChrootPath(self.chromeos_root, "/tmp") 1194*760c253cSXin Li ) 1195*760c253cSXin Li 1196*760c253cSXin Li command = "cd %s && tar xf %s" % ( 1197*760c253cSXin Li self.temp_dir, 1198*760c253cSXin Li os.path.join(cache_dir, AUTOTEST_TARBALL), 1199*760c253cSXin Li ) 1200*760c253cSXin Li ret = self.ce.RunCommand(command, print_to_console=False) 1201*760c253cSXin Li if ret: 1202*760c253cSXin Li raise RuntimeError("Could not untar cached tarball") 1203*760c253cSXin Li self.results_dir = self.temp_dir 1204*760c253cSXin Li self.results_file = self.GetDataMeasurementsFiles() 1205*760c253cSXin Li self.perf_data_files = self.GetPerfDataFiles() 1206*760c253cSXin Li self.perf_report_files = self.GetPerfReportFiles() 1207*760c253cSXin Li self.chrome_version = self.GetChromeVersionFromCache(cache_dir) 1208*760c253cSXin Li self.ProcessResults(use_cache=True) 1209*760c253cSXin Li 1210*760c253cSXin Li def CleanUp(self, rm_chroot_tmp): 1211*760c253cSXin Li if ( 1212*760c253cSXin Li rm_chroot_tmp 1213*760c253cSXin Li and self.results_dir 1214*760c253cSXin Li and self.results_dir != self.temp_dir 1215*760c253cSXin Li ): 1216*760c253cSXin Li dirname, basename = misc.GetRoot(self.results_dir) 1217*760c253cSXin Li if basename.find("test_that_results_") != -1: 1218*760c253cSXin Li command = "rm -rf %s" % self.results_dir 1219*760c253cSXin Li else: 1220*760c253cSXin Li command = "rm -rf %s" % dirname 1221*760c253cSXin Li self.ce.RunCommand(command) 1222*760c253cSXin Li if self.temp_dir: 1223*760c253cSXin Li command = "rm -rf %s" % self.temp_dir 1224*760c253cSXin Li self.ce.RunCommand(command) 1225*760c253cSXin Li 1226*760c253cSXin Li def CreateTarball(self, results_dir, tarball): 1227*760c253cSXin Li if not results_dir.strip(): 1228*760c253cSXin Li raise ValueError( 1229*760c253cSXin Li "Refusing to `tar` an empty results_dir: %r" % results_dir 1230*760c253cSXin Li ) 1231*760c253cSXin Li 1232*760c253cSXin Li ret = self.ce.RunCommand( 1233*760c253cSXin Li "cd %s && " 1234*760c253cSXin Li "tar " 1235*760c253cSXin Li "--exclude=var/spool " 1236*760c253cSXin Li "--exclude=var/log " 1237*760c253cSXin Li "-cjf %s ." % (results_dir, tarball) 1238*760c253cSXin Li ) 1239*760c253cSXin Li if ret: 1240*760c253cSXin Li raise RuntimeError("Couldn't compress test output directory.") 1241*760c253cSXin Li 1242*760c253cSXin Li def StoreToCacheDir(self, cache_dir, machine_manager, key_list): 1243*760c253cSXin Li # Create the dir if it doesn't exist. 1244*760c253cSXin Li temp_dir = tempfile.mkdtemp() 1245*760c253cSXin Li 1246*760c253cSXin Li # Store to the temp directory. 1247*760c253cSXin Li with open(os.path.join(temp_dir, RESULTS_FILE), "wb") as f: 1248*760c253cSXin Li pickle.dump(self.out, f) 1249*760c253cSXin Li pickle.dump(self.err, f) 1250*760c253cSXin Li pickle.dump(self.retval, f) 1251*760c253cSXin Li 1252*760c253cSXin Li if not test_flag.GetTestMode(): 1253*760c253cSXin Li with open( 1254*760c253cSXin Li os.path.join(temp_dir, CACHE_KEYS_FILE), "w", encoding="utf-8" 1255*760c253cSXin Li ) as f: 1256*760c253cSXin Li f.write("%s\n" % self.label.name) 1257*760c253cSXin Li f.write("%s\n" % self.label.chrome_version) 1258*760c253cSXin Li f.write("%s\n" % self.machine.checksum_string) 1259*760c253cSXin Li for k in key_list: 1260*760c253cSXin Li f.write(k) 1261*760c253cSXin Li f.write("\n") 1262*760c253cSXin Li 1263*760c253cSXin Li if self.results_dir: 1264*760c253cSXin Li tarball = os.path.join(temp_dir, AUTOTEST_TARBALL) 1265*760c253cSXin Li self.CreateTarball(self.results_dir, tarball) 1266*760c253cSXin Li 1267*760c253cSXin Li # Store machine info. 1268*760c253cSXin Li # TODO(asharif): Make machine_manager a singleton, and don't pass it into 1269*760c253cSXin Li # this function. 1270*760c253cSXin Li with open( 1271*760c253cSXin Li os.path.join(temp_dir, MACHINE_FILE), "w", encoding="utf-8" 1272*760c253cSXin Li ) as f: 1273*760c253cSXin Li f.write(machine_manager.machine_checksum_string[self.label.name]) 1274*760c253cSXin Li 1275*760c253cSXin Li if os.path.exists(cache_dir): 1276*760c253cSXin Li command = f"rm -rf {cache_dir}" 1277*760c253cSXin Li self.ce.RunCommand(command) 1278*760c253cSXin Li 1279*760c253cSXin Li parent_dir = os.path.dirname(cache_dir) 1280*760c253cSXin Li command = f"mkdir -p {parent_dir} && " 1281*760c253cSXin Li command += f"chmod g+x {temp_dir} && " 1282*760c253cSXin Li command += f"mv {temp_dir} {cache_dir}" 1283*760c253cSXin Li ret = self.ce.RunCommand(command) 1284*760c253cSXin Li if ret: 1285*760c253cSXin Li command = f"rm -rf {temp_dir}" 1286*760c253cSXin Li self.ce.RunCommand(command) 1287*760c253cSXin Li raise RuntimeError( 1288*760c253cSXin Li "Could not move dir %s to dir %s" % (temp_dir, cache_dir) 1289*760c253cSXin Li ) 1290*760c253cSXin Li 1291*760c253cSXin Li @classmethod 1292*760c253cSXin Li def CreateFromRun( 1293*760c253cSXin Li cls, 1294*760c253cSXin Li logger, 1295*760c253cSXin Li log_level, 1296*760c253cSXin Li label, 1297*760c253cSXin Li machine, 1298*760c253cSXin Li out, 1299*760c253cSXin Li err, 1300*760c253cSXin Li retval, 1301*760c253cSXin Li test, 1302*760c253cSXin Li suite="telemetry_Crosperf", 1303*760c253cSXin Li cwp_dso="", 1304*760c253cSXin Li ): 1305*760c253cSXin Li if suite == "telemetry": 1306*760c253cSXin Li result = TelemetryResult(logger, label, log_level, machine) 1307*760c253cSXin Li else: 1308*760c253cSXin Li result = cls(logger, label, log_level, machine) 1309*760c253cSXin Li result.PopulateFromRun(out, err, retval, test, suite, cwp_dso) 1310*760c253cSXin Li return result 1311*760c253cSXin Li 1312*760c253cSXin Li @classmethod 1313*760c253cSXin Li def CreateFromCacheHit( 1314*760c253cSXin Li cls, 1315*760c253cSXin Li logger, 1316*760c253cSXin Li log_level, 1317*760c253cSXin Li label, 1318*760c253cSXin Li machine, 1319*760c253cSXin Li cache_dir, 1320*760c253cSXin Li test, 1321*760c253cSXin Li suite="telemetry_Crosperf", 1322*760c253cSXin Li cwp_dso="", 1323*760c253cSXin Li ): 1324*760c253cSXin Li if suite == "telemetry": 1325*760c253cSXin Li result = TelemetryResult(logger, label, log_level, machine) 1326*760c253cSXin Li else: 1327*760c253cSXin Li result = cls(logger, label, log_level, machine) 1328*760c253cSXin Li try: 1329*760c253cSXin Li result.PopulateFromCacheDir(cache_dir, test, suite, cwp_dso) 1330*760c253cSXin Li 1331*760c253cSXin Li except RuntimeError as e: 1332*760c253cSXin Li logger.LogError("Exception while using cache: %s" % e) 1333*760c253cSXin Li return None 1334*760c253cSXin Li return result 1335*760c253cSXin Li 1336*760c253cSXin Li 1337*760c253cSXin Liclass TelemetryResult(Result): 1338*760c253cSXin Li """Class to hold the results of a single Telemetry run.""" 1339*760c253cSXin Li 1340*760c253cSXin Li def PopulateFromRun(self, out, err, retval, test, suite, cwp_dso): 1341*760c253cSXin Li self.out = out 1342*760c253cSXin Li self.err = err 1343*760c253cSXin Li self.retval = retval 1344*760c253cSXin Li 1345*760c253cSXin Li self.ProcessResults() 1346*760c253cSXin Li 1347*760c253cSXin Li # pylint: disable=arguments-differ 1348*760c253cSXin Li def ProcessResults(self): 1349*760c253cSXin Li # The output is: 1350*760c253cSXin Li # url,average_commit_time (ms),... 1351*760c253cSXin Li # www.google.com,33.4,21.2,... 1352*760c253cSXin Li # We need to convert to this format: 1353*760c253cSXin Li # {"www.google.com:average_commit_time (ms)": "33.4", 1354*760c253cSXin Li # "www.google.com:...": "21.2"} 1355*760c253cSXin Li # Added note: Occasionally the output comes back 1356*760c253cSXin Li # with "JSON.stringify(window.automation.GetResults())" on 1357*760c253cSXin Li # the first line, and then the rest of the output as 1358*760c253cSXin Li # described above. 1359*760c253cSXin Li 1360*760c253cSXin Li lines = self.out.splitlines() 1361*760c253cSXin Li self.keyvals = {} 1362*760c253cSXin Li 1363*760c253cSXin Li if lines: 1364*760c253cSXin Li if lines[0].startswith("JSON.stringify"): 1365*760c253cSXin Li lines = lines[1:] 1366*760c253cSXin Li 1367*760c253cSXin Li if not lines: 1368*760c253cSXin Li return 1369*760c253cSXin Li labels = lines[0].split(",") 1370*760c253cSXin Li for line in lines[1:]: 1371*760c253cSXin Li fields = line.split(",") 1372*760c253cSXin Li if len(fields) != len(labels): 1373*760c253cSXin Li continue 1374*760c253cSXin Li for i in range(1, len(labels)): 1375*760c253cSXin Li key = "%s %s" % (fields[0], labels[i]) 1376*760c253cSXin Li value = fields[i] 1377*760c253cSXin Li self.keyvals[key] = value 1378*760c253cSXin Li self.keyvals["retval"] = self.retval 1379*760c253cSXin Li 1380*760c253cSXin Li def PopulateFromCacheDir(self, cache_dir, test, suite, cwp_dso): 1381*760c253cSXin Li self.test_name = test 1382*760c253cSXin Li self.suite = suite 1383*760c253cSXin Li self.cwp_dso = cwp_dso 1384*760c253cSXin Li with open(os.path.join(cache_dir, RESULTS_FILE), "rb") as f: 1385*760c253cSXin Li self.out = pickle.load(f) 1386*760c253cSXin Li self.err = pickle.load(f) 1387*760c253cSXin Li self.retval = pickle.load(f) 1388*760c253cSXin Li 1389*760c253cSXin Li self.chrome_version = super( 1390*760c253cSXin Li TelemetryResult, self 1391*760c253cSXin Li ).GetChromeVersionFromCache(cache_dir) 1392*760c253cSXin Li self.ProcessResults() 1393*760c253cSXin Li 1394*760c253cSXin Li 1395*760c253cSXin Liclass CacheConditions(object): 1396*760c253cSXin Li """Various Cache condition values, for export.""" 1397*760c253cSXin Li 1398*760c253cSXin Li # Cache hit only if the result file exists. 1399*760c253cSXin Li CACHE_FILE_EXISTS = 0 1400*760c253cSXin Li 1401*760c253cSXin Li # Cache hit if the checksum of cpuinfo and totalmem of 1402*760c253cSXin Li # the cached result and the new run match. 1403*760c253cSXin Li MACHINES_MATCH = 1 1404*760c253cSXin Li 1405*760c253cSXin Li # Cache hit if the image checksum of the cached result and the new run match. 1406*760c253cSXin Li CHECKSUMS_MATCH = 2 1407*760c253cSXin Li 1408*760c253cSXin Li # Cache hit only if the cached result was successful 1409*760c253cSXin Li RUN_SUCCEEDED = 3 1410*760c253cSXin Li 1411*760c253cSXin Li # Never a cache hit. 1412*760c253cSXin Li FALSE = 4 1413*760c253cSXin Li 1414*760c253cSXin Li # Cache hit if the image path matches the cached image path. 1415*760c253cSXin Li IMAGE_PATH_MATCH = 5 1416*760c253cSXin Li 1417*760c253cSXin Li # Cache hit if the uuid of hard disk mataches the cached one 1418*760c253cSXin Li 1419*760c253cSXin Li SAME_MACHINE_MATCH = 6 1420*760c253cSXin Li 1421*760c253cSXin Li 1422*760c253cSXin Liclass ResultsCache(object): 1423*760c253cSXin Li """Class to handle the cache for storing/retrieving test run results. 1424*760c253cSXin Li 1425*760c253cSXin Li This class manages the key of the cached runs without worrying about what 1426*760c253cSXin Li is exactly stored (value). The value generation is handled by the Results 1427*760c253cSXin Li class. 1428*760c253cSXin Li """ 1429*760c253cSXin Li 1430*760c253cSXin Li CACHE_VERSION = 6 1431*760c253cSXin Li 1432*760c253cSXin Li def __init__(self): 1433*760c253cSXin Li # Proper initialization happens in the Init function below. 1434*760c253cSXin Li self.chromeos_image = None 1435*760c253cSXin Li self.chromeos_root = None 1436*760c253cSXin Li self.test_name = None 1437*760c253cSXin Li self.iteration = None 1438*760c253cSXin Li self.test_args = None 1439*760c253cSXin Li self.profiler_args = None 1440*760c253cSXin Li self.board = None 1441*760c253cSXin Li self.cache_conditions = None 1442*760c253cSXin Li self.machine_manager = None 1443*760c253cSXin Li self.machine = None 1444*760c253cSXin Li self._logger = None 1445*760c253cSXin Li self.ce = None 1446*760c253cSXin Li self.label = None 1447*760c253cSXin Li self.share_cache = None 1448*760c253cSXin Li self.suite = None 1449*760c253cSXin Li self.log_level = None 1450*760c253cSXin Li self.show_all = None 1451*760c253cSXin Li self.run_local = None 1452*760c253cSXin Li self.cwp_dso = None 1453*760c253cSXin Li 1454*760c253cSXin Li def Init( 1455*760c253cSXin Li self, 1456*760c253cSXin Li chromeos_image, 1457*760c253cSXin Li chromeos_root, 1458*760c253cSXin Li test_name, 1459*760c253cSXin Li iteration, 1460*760c253cSXin Li test_args, 1461*760c253cSXin Li profiler_args, 1462*760c253cSXin Li machine_manager, 1463*760c253cSXin Li machine, 1464*760c253cSXin Li board, 1465*760c253cSXin Li cache_conditions, 1466*760c253cSXin Li logger_to_use, 1467*760c253cSXin Li log_level, 1468*760c253cSXin Li label, 1469*760c253cSXin Li share_cache, 1470*760c253cSXin Li suite, 1471*760c253cSXin Li show_all_results, 1472*760c253cSXin Li run_local, 1473*760c253cSXin Li cwp_dso, 1474*760c253cSXin Li ): 1475*760c253cSXin Li self.chromeos_image = chromeos_image 1476*760c253cSXin Li self.chromeos_root = chromeos_root 1477*760c253cSXin Li self.test_name = test_name 1478*760c253cSXin Li self.iteration = iteration 1479*760c253cSXin Li self.test_args = test_args 1480*760c253cSXin Li self.profiler_args = profiler_args 1481*760c253cSXin Li self.board = board 1482*760c253cSXin Li self.cache_conditions = cache_conditions 1483*760c253cSXin Li self.machine_manager = machine_manager 1484*760c253cSXin Li self.machine = machine 1485*760c253cSXin Li self._logger = logger_to_use 1486*760c253cSXin Li self.ce = command_executer.GetCommandExecuter( 1487*760c253cSXin Li self._logger, log_level=log_level 1488*760c253cSXin Li ) 1489*760c253cSXin Li self.label = label 1490*760c253cSXin Li self.share_cache = share_cache 1491*760c253cSXin Li self.suite = suite 1492*760c253cSXin Li self.log_level = log_level 1493*760c253cSXin Li self.show_all = show_all_results 1494*760c253cSXin Li self.run_local = run_local 1495*760c253cSXin Li self.cwp_dso = cwp_dso 1496*760c253cSXin Li 1497*760c253cSXin Li def GetCacheDirForRead(self): 1498*760c253cSXin Li matching_dirs = [] 1499*760c253cSXin Li for glob_path in self.FormCacheDir(self.GetCacheKeyList(True)): 1500*760c253cSXin Li matching_dirs += glob.glob(glob_path) 1501*760c253cSXin Li 1502*760c253cSXin Li if matching_dirs: 1503*760c253cSXin Li # Cache file found. 1504*760c253cSXin Li return matching_dirs[0] 1505*760c253cSXin Li return None 1506*760c253cSXin Li 1507*760c253cSXin Li def GetCacheDirForWrite(self, get_keylist=False): 1508*760c253cSXin Li cache_path = self.FormCacheDir(self.GetCacheKeyList(False))[0] 1509*760c253cSXin Li if get_keylist: 1510*760c253cSXin Li args_str = "%s_%s_%s" % ( 1511*760c253cSXin Li self.test_args, 1512*760c253cSXin Li self.profiler_args, 1513*760c253cSXin Li self.run_local, 1514*760c253cSXin Li ) 1515*760c253cSXin Li version, image = results_report.ParseChromeosImage( 1516*760c253cSXin Li self.label.chromeos_image 1517*760c253cSXin Li ) 1518*760c253cSXin Li keylist = [ 1519*760c253cSXin Li version, 1520*760c253cSXin Li image, 1521*760c253cSXin Li self.label.board, 1522*760c253cSXin Li self.machine.name, 1523*760c253cSXin Li self.test_name, 1524*760c253cSXin Li str(self.iteration), 1525*760c253cSXin Li args_str, 1526*760c253cSXin Li ] 1527*760c253cSXin Li return cache_path, keylist 1528*760c253cSXin Li return cache_path 1529*760c253cSXin Li 1530*760c253cSXin Li def FormCacheDir(self, list_of_strings): 1531*760c253cSXin Li cache_key = " ".join(list_of_strings) 1532*760c253cSXin Li cache_dir = misc.GetFilenameFromString(cache_key) 1533*760c253cSXin Li if self.label.cache_dir: 1534*760c253cSXin Li cache_home = os.path.abspath( 1535*760c253cSXin Li os.path.expanduser(self.label.cache_dir) 1536*760c253cSXin Li ) 1537*760c253cSXin Li cache_path = [os.path.join(cache_home, cache_dir)] 1538*760c253cSXin Li else: 1539*760c253cSXin Li cache_path = [os.path.join(SCRATCH_DIR, cache_dir)] 1540*760c253cSXin Li 1541*760c253cSXin Li if self.share_cache: 1542*760c253cSXin Li for path in [x.strip() for x in self.share_cache.split(",")]: 1543*760c253cSXin Li if os.path.exists(path): 1544*760c253cSXin Li cache_path.append(os.path.join(path, cache_dir)) 1545*760c253cSXin Li else: 1546*760c253cSXin Li self._logger.LogFatal( 1547*760c253cSXin Li "Unable to find shared cache: %s" % path 1548*760c253cSXin Li ) 1549*760c253cSXin Li 1550*760c253cSXin Li return cache_path 1551*760c253cSXin Li 1552*760c253cSXin Li def GetCacheKeyList(self, read): 1553*760c253cSXin Li if read and CacheConditions.MACHINES_MATCH not in self.cache_conditions: 1554*760c253cSXin Li machine_checksum = "*" 1555*760c253cSXin Li else: 1556*760c253cSXin Li machine_checksum = self.machine_manager.machine_checksum[ 1557*760c253cSXin Li self.label.name 1558*760c253cSXin Li ] 1559*760c253cSXin Li if ( 1560*760c253cSXin Li read 1561*760c253cSXin Li and CacheConditions.CHECKSUMS_MATCH not in self.cache_conditions 1562*760c253cSXin Li ): 1563*760c253cSXin Li checksum = "*" 1564*760c253cSXin Li elif self.label.image_type == "trybot": 1565*760c253cSXin Li checksum = hashlib.md5( 1566*760c253cSXin Li self.label.chromeos_image.encode("utf-8") 1567*760c253cSXin Li ).hexdigest() 1568*760c253cSXin Li elif self.label.image_type == "official": 1569*760c253cSXin Li checksum = "*" 1570*760c253cSXin Li else: 1571*760c253cSXin Li checksum = ImageChecksummer().Checksum(self.label, self.log_level) 1572*760c253cSXin Li 1573*760c253cSXin Li if ( 1574*760c253cSXin Li read 1575*760c253cSXin Li and CacheConditions.IMAGE_PATH_MATCH not in self.cache_conditions 1576*760c253cSXin Li ): 1577*760c253cSXin Li image_path_checksum = "*" 1578*760c253cSXin Li else: 1579*760c253cSXin Li image_path_checksum = hashlib.md5( 1580*760c253cSXin Li self.chromeos_image.encode("utf-8") 1581*760c253cSXin Li ).hexdigest() 1582*760c253cSXin Li 1583*760c253cSXin Li machine_id_checksum = "" 1584*760c253cSXin Li if ( 1585*760c253cSXin Li read 1586*760c253cSXin Li and CacheConditions.SAME_MACHINE_MATCH not in self.cache_conditions 1587*760c253cSXin Li ): 1588*760c253cSXin Li machine_id_checksum = "*" 1589*760c253cSXin Li else: 1590*760c253cSXin Li if self.machine and self.machine.name in self.label.remote: 1591*760c253cSXin Li machine_id_checksum = self.machine.machine_id_checksum 1592*760c253cSXin Li else: 1593*760c253cSXin Li for machine in self.machine_manager.GetMachines(self.label): 1594*760c253cSXin Li if machine.name == self.label.remote[0]: 1595*760c253cSXin Li machine_id_checksum = machine.machine_id_checksum 1596*760c253cSXin Li break 1597*760c253cSXin Li 1598*760c253cSXin Li temp_test_args = "%s %s %s" % ( 1599*760c253cSXin Li self.test_args, 1600*760c253cSXin Li self.profiler_args, 1601*760c253cSXin Li self.run_local, 1602*760c253cSXin Li ) 1603*760c253cSXin Li test_args_checksum = hashlib.md5( 1604*760c253cSXin Li temp_test_args.encode("utf-8") 1605*760c253cSXin Li ).hexdigest() 1606*760c253cSXin Li return ( 1607*760c253cSXin Li image_path_checksum, 1608*760c253cSXin Li self.test_name, 1609*760c253cSXin Li str(self.iteration), 1610*760c253cSXin Li test_args_checksum, 1611*760c253cSXin Li checksum, 1612*760c253cSXin Li machine_checksum, 1613*760c253cSXin Li machine_id_checksum, 1614*760c253cSXin Li str(self.CACHE_VERSION), 1615*760c253cSXin Li ) 1616*760c253cSXin Li 1617*760c253cSXin Li def ReadResult(self): 1618*760c253cSXin Li if CacheConditions.FALSE in self.cache_conditions: 1619*760c253cSXin Li cache_dir = self.GetCacheDirForWrite() 1620*760c253cSXin Li command = "rm -rf %s" % (cache_dir,) 1621*760c253cSXin Li self.ce.RunCommand(command) 1622*760c253cSXin Li return None 1623*760c253cSXin Li cache_dir = self.GetCacheDirForRead() 1624*760c253cSXin Li 1625*760c253cSXin Li if not cache_dir: 1626*760c253cSXin Li return None 1627*760c253cSXin Li 1628*760c253cSXin Li if not os.path.isdir(cache_dir): 1629*760c253cSXin Li return None 1630*760c253cSXin Li 1631*760c253cSXin Li if self.log_level == "verbose": 1632*760c253cSXin Li self._logger.LogOutput( 1633*760c253cSXin Li "Trying to read from cache dir: %s" % cache_dir 1634*760c253cSXin Li ) 1635*760c253cSXin Li result = Result.CreateFromCacheHit( 1636*760c253cSXin Li self._logger, 1637*760c253cSXin Li self.log_level, 1638*760c253cSXin Li self.label, 1639*760c253cSXin Li self.machine, 1640*760c253cSXin Li cache_dir, 1641*760c253cSXin Li self.test_name, 1642*760c253cSXin Li self.suite, 1643*760c253cSXin Li self.cwp_dso, 1644*760c253cSXin Li ) 1645*760c253cSXin Li if not result: 1646*760c253cSXin Li return None 1647*760c253cSXin Li 1648*760c253cSXin Li if ( 1649*760c253cSXin Li result.retval == 0 1650*760c253cSXin Li or CacheConditions.RUN_SUCCEEDED not in self.cache_conditions 1651*760c253cSXin Li ): 1652*760c253cSXin Li return result 1653*760c253cSXin Li 1654*760c253cSXin Li return None 1655*760c253cSXin Li 1656*760c253cSXin Li def StoreResult(self, result): 1657*760c253cSXin Li cache_dir, keylist = self.GetCacheDirForWrite(get_keylist=True) 1658*760c253cSXin Li result.StoreToCacheDir(cache_dir, self.machine_manager, keylist) 1659*760c253cSXin Li 1660*760c253cSXin Li 1661*760c253cSXin Liclass MockResultsCache(ResultsCache): 1662*760c253cSXin Li """Class for mock testing, corresponding to ResultsCache class.""" 1663*760c253cSXin Li 1664*760c253cSXin Li # FIXME: pylint complains about this mock init method, we should probably 1665*760c253cSXin Li # replace all Mock classes in Crosperf with simple Mock.mock(). 1666*760c253cSXin Li # pylint: disable=arguments-differ 1667*760c253cSXin Li def Init(self, *args): 1668*760c253cSXin Li pass 1669*760c253cSXin Li 1670*760c253cSXin Li def ReadResult(self): 1671*760c253cSXin Li return None 1672*760c253cSXin Li 1673*760c253cSXin Li def StoreResult(self, result): 1674*760c253cSXin Li pass 1675*760c253cSXin Li 1676*760c253cSXin Li 1677*760c253cSXin Liclass MockResult(Result): 1678*760c253cSXin Li """Class for mock testing, corresponding to Result class.""" 1679*760c253cSXin Li 1680*760c253cSXin Li def PopulateFromRun(self, out, err, retval, test, suite, cwp_dso): 1681*760c253cSXin Li self.out = out 1682*760c253cSXin Li self.err = err 1683*760c253cSXin Li self.retval = retval 1684