xref: /aosp_15_r20/external/toolchain-utils/crosperf/experiment_file.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1*760c253cSXin Li# -*- coding: utf-8 -*-
2*760c253cSXin Li# Copyright 2011 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"""The experiment file module. It manages the input file of crosperf."""
7*760c253cSXin Li
8*760c253cSXin Li
9*760c253cSXin Liimport os.path
10*760c253cSXin Liimport re
11*760c253cSXin Li
12*760c253cSXin Lifrom settings_factory import SettingsFactory
13*760c253cSXin Li
14*760c253cSXin Li
15*760c253cSXin Liclass ExperimentFile(object):
16*760c253cSXin Li    """Class for parsing the experiment file format.
17*760c253cSXin Li
18*760c253cSXin Li    The grammar for this format is:
19*760c253cSXin Li
20*760c253cSXin Li    experiment = { _FIELD_VALUE_RE | settings }
21*760c253cSXin Li    settings = _OPEN_SETTINGS_RE
22*760c253cSXin Li               { _FIELD_VALUE_RE }
23*760c253cSXin Li               _CLOSE_SETTINGS_RE
24*760c253cSXin Li
25*760c253cSXin Li    Where the regexes are terminals defined below. This results in an format
26*760c253cSXin Li    which looks something like:
27*760c253cSXin Li
28*760c253cSXin Li    field_name: value
29*760c253cSXin Li    settings_type: settings_name {
30*760c253cSXin Li      field_name: value
31*760c253cSXin Li      field_name: value
32*760c253cSXin Li    }
33*760c253cSXin Li    """
34*760c253cSXin Li
35*760c253cSXin Li    # Field regex, e.g. "iterations: 3"
36*760c253cSXin Li    _FIELD_VALUE_RE = re.compile(r"(\+)?\s*(\w+?)(?:\.(\S+))?\s*:\s*(.*)")
37*760c253cSXin Li    # Open settings regex, e.g. "label {"
38*760c253cSXin Li    _OPEN_SETTINGS_RE = re.compile(r"(?:([\w.-]+):)?\s*([\w.-]+)\s*{")
39*760c253cSXin Li    # Close settings regex.
40*760c253cSXin Li    _CLOSE_SETTINGS_RE = re.compile(r"}")
41*760c253cSXin Li
42*760c253cSXin Li    def __init__(self, experiment_file, overrides=None):
43*760c253cSXin Li        """Construct object from file-like experiment_file.
44*760c253cSXin Li
45*760c253cSXin Li        Args:
46*760c253cSXin Li          experiment_file: file-like object with text description of experiment.
47*760c253cSXin Li          overrides: A settings object that will override fields in other settings.
48*760c253cSXin Li
49*760c253cSXin Li        Raises:
50*760c253cSXin Li          Exception: if invalid build type or description is invalid.
51*760c253cSXin Li        """
52*760c253cSXin Li        self.all_settings = []
53*760c253cSXin Li        self.global_settings = SettingsFactory().GetSettings("global", "global")
54*760c253cSXin Li        self.all_settings.append(self.global_settings)
55*760c253cSXin Li
56*760c253cSXin Li        self._Parse(experiment_file)
57*760c253cSXin Li
58*760c253cSXin Li        for settings in self.all_settings:
59*760c253cSXin Li            settings.Inherit()
60*760c253cSXin Li            settings.Validate()
61*760c253cSXin Li            if overrides:
62*760c253cSXin Li                settings.Override(overrides)
63*760c253cSXin Li
64*760c253cSXin Li    def GetSettings(self, settings_type):
65*760c253cSXin Li        """Return nested fields from the experiment file."""
66*760c253cSXin Li        res = []
67*760c253cSXin Li        for settings in self.all_settings:
68*760c253cSXin Li            if settings.settings_type == settings_type:
69*760c253cSXin Li                res.append(settings)
70*760c253cSXin Li        return res
71*760c253cSXin Li
72*760c253cSXin Li    def GetGlobalSettings(self):
73*760c253cSXin Li        """Return the global fields from the experiment file."""
74*760c253cSXin Li        return self.global_settings
75*760c253cSXin Li
76*760c253cSXin Li    def _ParseField(self, reader):
77*760c253cSXin Li        """Parse a key/value field."""
78*760c253cSXin Li        line = reader.CurrentLine().strip()
79*760c253cSXin Li        match = ExperimentFile._FIELD_VALUE_RE.match(line)
80*760c253cSXin Li        append, name, _, text_value = match.groups()
81*760c253cSXin Li        return (name, text_value, append)
82*760c253cSXin Li
83*760c253cSXin Li    def _ParseSettings(self, reader):
84*760c253cSXin Li        """Parse a settings block."""
85*760c253cSXin Li        line = reader.CurrentLine().strip()
86*760c253cSXin Li        match = ExperimentFile._OPEN_SETTINGS_RE.match(line)
87*760c253cSXin Li        settings_type = match.group(1)
88*760c253cSXin Li        if settings_type is None:
89*760c253cSXin Li            settings_type = ""
90*760c253cSXin Li        settings_name = match.group(2)
91*760c253cSXin Li        settings = SettingsFactory().GetSettings(settings_name, settings_type)
92*760c253cSXin Li        settings.SetParentSettings(self.global_settings)
93*760c253cSXin Li
94*760c253cSXin Li        while reader.NextLine():
95*760c253cSXin Li            line = reader.CurrentLine().strip()
96*760c253cSXin Li
97*760c253cSXin Li            if not line:
98*760c253cSXin Li                continue
99*760c253cSXin Li
100*760c253cSXin Li            if ExperimentFile._FIELD_VALUE_RE.match(line):
101*760c253cSXin Li                field = self._ParseField(reader)
102*760c253cSXin Li                settings.SetField(field[0], field[1], field[2])
103*760c253cSXin Li            elif ExperimentFile._CLOSE_SETTINGS_RE.match(line):
104*760c253cSXin Li                return settings, settings_type
105*760c253cSXin Li
106*760c253cSXin Li        raise EOFError("Unexpected EOF while parsing settings block.")
107*760c253cSXin Li
108*760c253cSXin Li    def _Parse(self, experiment_file):
109*760c253cSXin Li        """Parse experiment file and create settings."""
110*760c253cSXin Li        reader = ExperimentFileReader(experiment_file)
111*760c253cSXin Li        settings_names = {}
112*760c253cSXin Li        try:
113*760c253cSXin Li            while reader.NextLine():
114*760c253cSXin Li                line = reader.CurrentLine().strip()
115*760c253cSXin Li
116*760c253cSXin Li                if not line:
117*760c253cSXin Li                    continue
118*760c253cSXin Li
119*760c253cSXin Li                if ExperimentFile._OPEN_SETTINGS_RE.match(line):
120*760c253cSXin Li                    new_settings, settings_type = self._ParseSettings(reader)
121*760c253cSXin Li                    # We will allow benchmarks with duplicated settings name for now.
122*760c253cSXin Li                    # Further decision will be made when parsing benchmark details in
123*760c253cSXin Li                    # ExperimentFactory.GetExperiment().
124*760c253cSXin Li                    if settings_type != "benchmark":
125*760c253cSXin Li                        if new_settings.name in settings_names:
126*760c253cSXin Li                            raise SyntaxError(
127*760c253cSXin Li                                "Duplicate settings name: '%s'."
128*760c253cSXin Li                                % new_settings.name
129*760c253cSXin Li                            )
130*760c253cSXin Li                        settings_names[new_settings.name] = True
131*760c253cSXin Li                    self.all_settings.append(new_settings)
132*760c253cSXin Li                elif ExperimentFile._FIELD_VALUE_RE.match(line):
133*760c253cSXin Li                    field = self._ParseField(reader)
134*760c253cSXin Li                    self.global_settings.SetField(field[0], field[1], field[2])
135*760c253cSXin Li                else:
136*760c253cSXin Li                    raise IOError("Unexpected line.")
137*760c253cSXin Li        except Exception as err:
138*760c253cSXin Li            raise RuntimeError(
139*760c253cSXin Li                "Line %d: %s\n==> %s"
140*760c253cSXin Li                % (reader.LineNo(), str(err), reader.CurrentLine(False))
141*760c253cSXin Li            )
142*760c253cSXin Li
143*760c253cSXin Li    def Canonicalize(self):
144*760c253cSXin Li        """Convert parsed experiment file back into an experiment file."""
145*760c253cSXin Li        res = ""
146*760c253cSXin Li        board = ""
147*760c253cSXin Li        for field_name in self.global_settings.fields:
148*760c253cSXin Li            field = self.global_settings.fields[field_name]
149*760c253cSXin Li            if field.assigned:
150*760c253cSXin Li                res += "%s: %s\n" % (field.name, field.GetString())
151*760c253cSXin Li            if field.name == "board":
152*760c253cSXin Li                board = field.GetString()
153*760c253cSXin Li        res += "\n"
154*760c253cSXin Li
155*760c253cSXin Li        for settings in self.all_settings:
156*760c253cSXin Li            if settings.settings_type != "global":
157*760c253cSXin Li                res += "%s: %s {\n" % (settings.settings_type, settings.name)
158*760c253cSXin Li                for field_name in settings.fields:
159*760c253cSXin Li                    field = settings.fields[field_name]
160*760c253cSXin Li                    if field.assigned:
161*760c253cSXin Li                        res += "\t%s: %s\n" % (field.name, field.GetString())
162*760c253cSXin Li                        if field.name == "chromeos_image":
163*760c253cSXin Li                            real_file = os.path.realpath(
164*760c253cSXin Li                                os.path.expanduser(field.GetString())
165*760c253cSXin Li                            )
166*760c253cSXin Li                            if real_file != field.GetString():
167*760c253cSXin Li                                res += "\t#actual_image: %s\n" % real_file
168*760c253cSXin Li                        if field.name == "build":
169*760c253cSXin Li                            chromeos_root_field = settings.fields[
170*760c253cSXin Li                                "chromeos_root"
171*760c253cSXin Li                            ]
172*760c253cSXin Li                            if chromeos_root_field:
173*760c253cSXin Li                                chromeos_root = chromeos_root_field.GetString()
174*760c253cSXin Li                            value = field.GetString()
175*760c253cSXin Li                            autotest_field = settings.fields["autotest_path"]
176*760c253cSXin Li                            autotest_path = ""
177*760c253cSXin Li                            if autotest_field.assigned:
178*760c253cSXin Li                                autotest_path = autotest_field.GetString()
179*760c253cSXin Li                            debug_field = settings.fields["debug_path"]
180*760c253cSXin Li                            debug_path = ""
181*760c253cSXin Li                            if debug_field.assigned:
182*760c253cSXin Li                                debug_path = autotest_field.GetString()
183*760c253cSXin Li                            # Do not download the debug symbols since this function is for
184*760c253cSXin Li                            # canonicalizing experiment file.
185*760c253cSXin Li                            downlad_debug = False
186*760c253cSXin Li                            (
187*760c253cSXin Li                                image_path,
188*760c253cSXin Li                                autotest_path,
189*760c253cSXin Li                                debug_path,
190*760c253cSXin Li                            ) = settings.GetXbuddyPath(
191*760c253cSXin Li                                value,
192*760c253cSXin Li                                autotest_path,
193*760c253cSXin Li                                debug_path,
194*760c253cSXin Li                                board,
195*760c253cSXin Li                                chromeos_root,
196*760c253cSXin Li                                "quiet",
197*760c253cSXin Li                                downlad_debug,
198*760c253cSXin Li                            )
199*760c253cSXin Li                            res += "\t#actual_image: %s\n" % image_path
200*760c253cSXin Li                            if not autotest_field.assigned:
201*760c253cSXin Li                                res += (
202*760c253cSXin Li                                    "\t#actual_autotest_path: %s\n"
203*760c253cSXin Li                                    % autotest_path
204*760c253cSXin Li                                )
205*760c253cSXin Li                            if not debug_field.assigned:
206*760c253cSXin Li                                res += "\t#actual_debug_path: %s\n" % debug_path
207*760c253cSXin Li
208*760c253cSXin Li                res += "}\n\n"
209*760c253cSXin Li
210*760c253cSXin Li        return res
211*760c253cSXin Li
212*760c253cSXin Li
213*760c253cSXin Liclass ExperimentFileReader(object):
214*760c253cSXin Li    """Handle reading lines from an experiment file."""
215*760c253cSXin Li
216*760c253cSXin Li    def __init__(self, file_object):
217*760c253cSXin Li        self.file_object = file_object
218*760c253cSXin Li        self.current_line = None
219*760c253cSXin Li        self.current_line_no = 0
220*760c253cSXin Li
221*760c253cSXin Li    def CurrentLine(self, strip_comment=True):
222*760c253cSXin Li        """Return the next line from the file, without advancing the iterator."""
223*760c253cSXin Li        if strip_comment:
224*760c253cSXin Li            return self._StripComment(self.current_line)
225*760c253cSXin Li        return self.current_line
226*760c253cSXin Li
227*760c253cSXin Li    def NextLine(self, strip_comment=True):
228*760c253cSXin Li        """Advance the iterator and return the next line of the file."""
229*760c253cSXin Li        self.current_line_no += 1
230*760c253cSXin Li        self.current_line = self.file_object.readline()
231*760c253cSXin Li        return self.CurrentLine(strip_comment)
232*760c253cSXin Li
233*760c253cSXin Li    def _StripComment(self, line):
234*760c253cSXin Li        """Strip comments starting with # from a line."""
235*760c253cSXin Li        if "#" in line:
236*760c253cSXin Li            line = line[: line.find("#")] + line[-1]
237*760c253cSXin Li        return line
238*760c253cSXin Li
239*760c253cSXin Li    def LineNo(self):
240*760c253cSXin Li        """Return the current line number."""
241*760c253cSXin Li        return self.current_line_no
242