xref: /aosp_15_r20/external/toolchain-utils/crosperf/results_cache.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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