xref: /llvm-project/cross-project-tests/debuginfo-tests/dexter/dex/tools/clang_opt_bisect/Tool.py (revision 4a351ef70aa6e34a8eb5a8655473531881225fa4)
11364750dSJames Henderson# DExTer : Debugging Experience Tester
21364750dSJames Henderson# ~~~~~~   ~         ~~         ~   ~~
31364750dSJames Henderson#
41364750dSJames Henderson# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
51364750dSJames Henderson# See https://llvm.org/LICENSE.txt for license information.
61364750dSJames Henderson# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
71364750dSJames Henderson"""Clang opt-bisect tool."""
81364750dSJames Henderson
91364750dSJames Hendersonfrom collections import defaultdict
101364750dSJames Hendersonimport os
111364750dSJames Hendersonimport csv
121364750dSJames Hendersonimport re
131364750dSJames Hendersonimport pickle
141364750dSJames Henderson
151364750dSJames Hendersonfrom dex.command.ParseCommand import get_command_infos
161364750dSJames Hendersonfrom dex.debugger.Debuggers import run_debugger_subprocess
171364750dSJames Hendersonfrom dex.debugger.DebuggerControllers.DefaultController import DefaultController
181364750dSJames Hendersonfrom dex.dextIR.DextIR import DextIR
191364750dSJames Hendersonfrom dex.heuristic import Heuristic
201364750dSJames Hendersonfrom dex.tools import TestToolBase
211364750dSJames Hendersonfrom dex.utils.Exceptions import DebuggerException, Error
221364750dSJames Hendersonfrom dex.utils.Exceptions import BuildScriptException, HeuristicException
231364750dSJames Hendersonfrom dex.utils.PrettyOutputBase import Stream
241364750dSJames Hendersonfrom dex.utils.ReturnCode import ReturnCode
251364750dSJames Henderson
261364750dSJames Henderson
271364750dSJames Hendersonclass BisectPass(object):
281364750dSJames Henderson    def __init__(self, no, description, description_no_loc):
291364750dSJames Henderson        self.no = no
301364750dSJames Henderson        self.description = description
311364750dSJames Henderson        self.description_no_loc = description_no_loc
321364750dSJames Henderson
331364750dSJames Henderson        self.penalty = 0
341364750dSJames Henderson        self.differences = []
351364750dSJames Henderson
361364750dSJames Henderson
371364750dSJames Hendersonclass Tool(TestToolBase):
381364750dSJames Henderson    """Use the LLVM "-opt-bisect-limit=<n>" flag to get information on the
391364750dSJames Henderson    contribution of each LLVM pass to the overall DExTer score when using
401364750dSJames Henderson    clang.
411364750dSJames Henderson
421364750dSJames Henderson    Clang is run multiple times, with an increasing value of n, measuring the
431364750dSJames Henderson    debugging experience at each value.
441364750dSJames Henderson    """
451364750dSJames Henderson
46*f98ee40fSTobias Hieta    _re_running_pass = re.compile(r"^BISECT\: running pass \((\d+)\) (.+?)( \(.+\))?$")
471364750dSJames Henderson
481364750dSJames Henderson    def __init__(self, *args, **kwargs):
491364750dSJames Henderson        super(Tool, self).__init__(*args, **kwargs)
501364750dSJames Henderson        self._all_bisect_pass_summary = defaultdict(list)
511364750dSJames Henderson
521364750dSJames Henderson    @property
531364750dSJames Henderson    def name(self):
54*f98ee40fSTobias Hieta        return "DExTer clang opt bisect"
551364750dSJames Henderson
561364750dSJames Henderson    def _get_bisect_limits(self):
571364750dSJames Henderson        options = self.context.options
581364750dSJames Henderson
591364750dSJames Henderson        max_limit = 999999
601364750dSJames Henderson        limits = [max_limit for _ in options.source_files]
611364750dSJames Henderson        all_passes = [
62*f98ee40fSTobias Hieta            l
63*f98ee40fSTobias Hieta            for l in self._clang_opt_bisect_build(limits)[1].splitlines()
64*f98ee40fSTobias Hieta            if l.startswith("BISECT: running pass (")
651364750dSJames Henderson        ]
661364750dSJames Henderson
671364750dSJames Henderson        results = []
681364750dSJames Henderson        for i, pass_ in enumerate(all_passes[1:]):
69*f98ee40fSTobias Hieta            if pass_.startswith("BISECT: running pass (1)"):
701364750dSJames Henderson                results.append(all_passes[i])
711364750dSJames Henderson        results.append(all_passes[-1])
721364750dSJames Henderson
73*f98ee40fSTobias Hieta        assert len(results) == len(options.source_files), (
74*f98ee40fSTobias Hieta            results,
75*f98ee40fSTobias Hieta            options.source_files,
76*f98ee40fSTobias Hieta        )
771364750dSJames Henderson
78*f98ee40fSTobias Hieta        limits = [int(Tool._re_running_pass.match(r).group(1)) for r in results]
791364750dSJames Henderson
801364750dSJames Henderson        return limits
811364750dSJames Henderson
821364750dSJames Henderson    def handle_options(self, defaults):
831364750dSJames Henderson        options = self.context.options
841364750dSJames Henderson        if "clang" not in options.builder.lower():
85*f98ee40fSTobias Hieta            raise Error(
86*f98ee40fSTobias Hieta                "--builder %s is not supported by the clang-opt-bisect tool - only 'clang' is "
87*f98ee40fSTobias Hieta                "supported " % options.builder
88*f98ee40fSTobias Hieta            )
891364750dSJames Henderson        super(Tool, self).handle_options(defaults)
901364750dSJames Henderson
911364750dSJames Henderson    def _init_debugger_controller(self):
921364750dSJames Henderson        step_collection = DextIR(
931364750dSJames Henderson            executable_path=self.context.options.executable,
941364750dSJames Henderson            source_paths=self.context.options.source_files,
95*f98ee40fSTobias Hieta            dexter_version=self.context.version,
96*f98ee40fSTobias Hieta        )
971364750dSJames Henderson
981364750dSJames Henderson        step_collection.commands, new_source_files = get_command_infos(
99*f98ee40fSTobias Hieta            self.context.options.source_files, self.context.options.source_root_dir
100*f98ee40fSTobias Hieta        )
1011364750dSJames Henderson        self.context.options.source_files.extend(list(new_source_files))
1021364750dSJames Henderson
1031364750dSJames Henderson        debugger_controller = DefaultController(self.context, step_collection)
1041364750dSJames Henderson        return debugger_controller
1051364750dSJames Henderson
1061364750dSJames Henderson    def _run_test(self, test_name):  # noqa
1071364750dSJames Henderson        options = self.context.options
1081364750dSJames Henderson
1091364750dSJames Henderson        per_pass_score = []
1101364750dSJames Henderson        current_bisect_pass_summary = defaultdict(list)
1111364750dSJames Henderson
1121364750dSJames Henderson        max_limits = self._get_bisect_limits()
1131364750dSJames Henderson        overall_limit = sum(max_limits)
1141364750dSJames Henderson        prev_score = 1.0
1151364750dSJames Henderson        prev_steps_str = None
1161364750dSJames Henderson
1171364750dSJames Henderson        for current_limit in range(overall_limit + 1):
1181364750dSJames Henderson            # Take the overall limit number and split it across buckets for
1191364750dSJames Henderson            # each source file.
1201364750dSJames Henderson            limit_remaining = current_limit
1211364750dSJames Henderson            file_limits = [0] * len(max_limits)
1221364750dSJames Henderson            for i, max_limit in enumerate(max_limits):
1231364750dSJames Henderson                if limit_remaining < max_limit:
1241364750dSJames Henderson                    file_limits[i] += limit_remaining
1251364750dSJames Henderson                    break
1261364750dSJames Henderson                else:
1271364750dSJames Henderson                    file_limits[i] = max_limit
1281364750dSJames Henderson                    limit_remaining -= file_limits[i]
1291364750dSJames Henderson
1301364750dSJames Henderson            f = [l for l in file_limits if l]
1311364750dSJames Henderson            current_file_index = len(f) - 1 if f else 0
1321364750dSJames Henderson
1331364750dSJames Henderson            _, err, builderIR = self._clang_opt_bisect_build(file_limits)
1341364750dSJames Henderson            err_lines = err.splitlines()
1351364750dSJames Henderson            # Find the last line that specified a running pass.
1361364750dSJames Henderson            for l in err_lines[::-1]:
1371364750dSJames Henderson                match = Tool._re_running_pass.match(l)
1381364750dSJames Henderson                if match:
1391364750dSJames Henderson                    pass_info = match.groups()
1401364750dSJames Henderson                    break
1411364750dSJames Henderson            else:
1421364750dSJames Henderson                pass_info = (0, None, None)
1431364750dSJames Henderson
1441364750dSJames Henderson            try:
1451364750dSJames Henderson                debugger_controller = self._init_debugger_controller()
1461364750dSJames Henderson                debugger_controller = run_debugger_subprocess(
147*f98ee40fSTobias Hieta                    debugger_controller, self.context.working_directory.path
148*f98ee40fSTobias Hieta                )
1491364750dSJames Henderson                steps = debugger_controller.step_collection
1501364750dSJames Henderson            except DebuggerException:
1511364750dSJames Henderson                steps = DextIR(
1521364750dSJames Henderson                    executable_path=self.context.options.executable,
1531364750dSJames Henderson                    source_paths=self.context.options.source_files,
154*f98ee40fSTobias Hieta                    dexter_version=self.context.version,
155*f98ee40fSTobias Hieta                )
1561364750dSJames Henderson
1571364750dSJames Henderson            steps.builder = builderIR
1581364750dSJames Henderson
1591364750dSJames Henderson            try:
1601364750dSJames Henderson                heuristic = Heuristic(self.context, steps)
1611364750dSJames Henderson            except HeuristicException as e:
1621364750dSJames Henderson                raise Error(e)
1631364750dSJames Henderson
1641364750dSJames Henderson            score_difference = heuristic.score - prev_score
1651364750dSJames Henderson            prev_score = heuristic.score
1661364750dSJames Henderson
1671364750dSJames Henderson            isnan = heuristic.score != heuristic.score
1681364750dSJames Henderson            if isnan or score_difference < 0:
169*f98ee40fSTobias Hieta                color1 = "r"
170*f98ee40fSTobias Hieta                color2 = "r"
1711364750dSJames Henderson            elif score_difference > 0:
172*f98ee40fSTobias Hieta                color1 = "g"
173*f98ee40fSTobias Hieta                color2 = "g"
1741364750dSJames Henderson            else:
175*f98ee40fSTobias Hieta                color1 = "y"
176*f98ee40fSTobias Hieta                color2 = "d"
1771364750dSJames Henderson
1781364750dSJames Henderson            summary = '<{}>running pass {}/{} on "{}"'.format(
179*f98ee40fSTobias Hieta                color2, pass_info[0], max_limits[current_file_index], test_name
180*f98ee40fSTobias Hieta            )
1811364750dSJames Henderson            if len(options.source_files) > 1:
182*f98ee40fSTobias Hieta                summary += " [{}/{}]".format(current_limit, overall_limit)
1831364750dSJames Henderson
184*f98ee40fSTobias Hieta            pass_text = "".join(p for p in pass_info[1:] if p)
185*f98ee40fSTobias Hieta            summary += ": {} <{}>{:+.4f}</> <{}>{}</></>\n".format(
186*f98ee40fSTobias Hieta                heuristic.summary_string, color1, score_difference, color2, pass_text
187*f98ee40fSTobias Hieta            )
1881364750dSJames Henderson
1891364750dSJames Henderson            self.context.o.auto(summary)
1901364750dSJames Henderson
1911364750dSJames Henderson            heuristic_verbose_output = heuristic.verbose_output
1921364750dSJames Henderson
1931364750dSJames Henderson            if options.verbose:
1941364750dSJames Henderson                self.context.o.auto(heuristic_verbose_output)
1951364750dSJames Henderson
1961364750dSJames Henderson            steps_str = str(steps)
1971364750dSJames Henderson            steps_changed = steps_str != prev_steps_str
1981364750dSJames Henderson            prev_steps_str = steps_str
1991364750dSJames Henderson
2002bd62e0bSOCHyams            # If a results directory has been specified and this is the first
2012bd62e0bSOCHyams            # pass or something has changed, write a text file containing
2022bd62e0bSOCHyams            # verbose information on the current status.
203*f98ee40fSTobias Hieta            if options.results_directory and (
204*f98ee40fSTobias Hieta                current_limit == 0 or score_difference or steps_changed
205*f98ee40fSTobias Hieta            ):
206*f98ee40fSTobias Hieta                file_name = "-".join(
207*f98ee40fSTobias Hieta                    str(s)
208*f98ee40fSTobias Hieta                    for s in [
209*f98ee40fSTobias Hieta                        "status",
210*f98ee40fSTobias Hieta                        test_name,
211*f98ee40fSTobias Hieta                        "{{:0>{}}}".format(len(str(overall_limit))).format(
212*f98ee40fSTobias Hieta                            current_limit
213*f98ee40fSTobias Hieta                        ),
214*f98ee40fSTobias Hieta                        "{:.4f}".format(heuristic.score).replace(".", "_"),
215*f98ee40fSTobias Hieta                        pass_info[1],
216*f98ee40fSTobias Hieta                    ]
217*f98ee40fSTobias Hieta                    if s is not None
218*f98ee40fSTobias Hieta                )
2191364750dSJames Henderson
220*f98ee40fSTobias Hieta                file_name = (
221*f98ee40fSTobias Hieta                    "".join(c for c in file_name if c.isalnum() or c in "()-_./ ")
222*f98ee40fSTobias Hieta                    .strip()
223*f98ee40fSTobias Hieta                    .replace(" ", "_")
224*f98ee40fSTobias Hieta                    .replace("/", "_")
225*f98ee40fSTobias Hieta                )
2261364750dSJames Henderson
227*f98ee40fSTobias Hieta                output_text_path = os.path.join(
228*f98ee40fSTobias Hieta                    options.results_directory, "{}.txt".format(file_name)
229*f98ee40fSTobias Hieta                )
230*f98ee40fSTobias Hieta                with open(output_text_path, "w") as fp:
231*f98ee40fSTobias Hieta                    self.context.o.auto(summary + "\n", stream=Stream(fp))
232*f98ee40fSTobias Hieta                    self.context.o.auto(str(steps) + "\n", stream=Stream(fp))
2331364750dSJames Henderson                    self.context.o.auto(
234*f98ee40fSTobias Hieta                        heuristic_verbose_output + "\n", stream=Stream(fp)
235*f98ee40fSTobias Hieta                    )
2361364750dSJames Henderson
237*f98ee40fSTobias Hieta                output_dextIR_path = os.path.join(
238*f98ee40fSTobias Hieta                    options.results_directory, "{}.dextIR".format(file_name)
239*f98ee40fSTobias Hieta                )
240*f98ee40fSTobias Hieta                with open(output_dextIR_path, "wb") as fp:
2411364750dSJames Henderson                    pickle.dump(steps, fp, protocol=pickle.HIGHEST_PROTOCOL)
2421364750dSJames Henderson
243*f98ee40fSTobias Hieta            per_pass_score.append((test_name, pass_text, heuristic.score))
2441364750dSJames Henderson
2451364750dSJames Henderson            if pass_info[1]:
246*f98ee40fSTobias Hieta                self._all_bisect_pass_summary[pass_info[1]].append(score_difference)
2471364750dSJames Henderson
248*f98ee40fSTobias Hieta                current_bisect_pass_summary[pass_info[1]].append(score_difference)
2491364750dSJames Henderson
2502bd62e0bSOCHyams        if options.results_directory:
2511364750dSJames Henderson            per_pass_score_path = os.path.join(
252*f98ee40fSTobias Hieta                options.results_directory, "{}-per_pass_score.csv".format(test_name)
253*f98ee40fSTobias Hieta            )
2541364750dSJames Henderson
255*f98ee40fSTobias Hieta            with open(per_pass_score_path, mode="w", newline="") as fp:
256*f98ee40fSTobias Hieta                writer = csv.writer(fp, delimiter=",")
257*f98ee40fSTobias Hieta                writer.writerow(["Source File", "Pass", "Score"])
2581364750dSJames Henderson
2591364750dSJames Henderson                for path, pass_, score in per_pass_score:
2601364750dSJames Henderson                    writer.writerow([path, pass_, score])
2611364750dSJames Henderson            self.context.o.blue('wrote "{}"\n'.format(per_pass_score_path))
2621364750dSJames Henderson
2631364750dSJames Henderson            pass_summary_path = os.path.join(
264*f98ee40fSTobias Hieta                options.results_directory, "{}-pass-summary.csv".format(test_name)
265*f98ee40fSTobias Hieta            )
2661364750dSJames Henderson
267*f98ee40fSTobias Hieta            self._write_pass_summary(pass_summary_path, current_bisect_pass_summary)
2681364750dSJames Henderson
2691364750dSJames Henderson    def _handle_results(self) -> ReturnCode:
2701364750dSJames Henderson        options = self.context.options
2712bd62e0bSOCHyams        if options.results_directory:
272*f98ee40fSTobias Hieta            pass_summary_path = os.path.join(
273*f98ee40fSTobias Hieta                options.results_directory, "overall-pass-summary.csv"
274*f98ee40fSTobias Hieta            )
2751364750dSJames Henderson
276*f98ee40fSTobias Hieta            self._write_pass_summary(pass_summary_path, self._all_bisect_pass_summary)
2771364750dSJames Henderson        return ReturnCode.OK
2781364750dSJames Henderson
2791364750dSJames Henderson    def _clang_opt_bisect_build(self, opt_bisect_limits):
2801364750dSJames Henderson        options = self.context.options
2811364750dSJames Henderson        compiler_options = [
282*f98ee40fSTobias Hieta            "{} -mllvm -opt-bisect-limit={}".format(options.cflags, opt_bisect_limit)
2831364750dSJames Henderson            for opt_bisect_limit in opt_bisect_limits
2841364750dSJames Henderson        ]
2851364750dSJames Henderson        linker_options = options.ldflags
2861364750dSJames Henderson
2871364750dSJames Henderson        try:
2881364750dSJames Henderson            return run_external_build_script(
2891364750dSJames Henderson                self.context,
2901364750dSJames Henderson                source_files=options.source_files,
2911364750dSJames Henderson                compiler_options=compiler_options,
2921364750dSJames Henderson                linker_options=linker_options,
2931364750dSJames Henderson                script_path=self.build_script,
294*f98ee40fSTobias Hieta                executable_file=options.executable,
295*f98ee40fSTobias Hieta            )
2961364750dSJames Henderson        except BuildScriptException as e:
2971364750dSJames Henderson            raise Error(e)
2981364750dSJames Henderson
2991364750dSJames Henderson    def _write_pass_summary(self, path, pass_summary):
3001364750dSJames Henderson        # Get a list of tuples.
3011364750dSJames Henderson        pass_summary_list = list(pass_summary.items())
3021364750dSJames Henderson
3031364750dSJames Henderson        for i, item in enumerate(pass_summary_list):
3041364750dSJames Henderson            # Add elems for the sum, min, and max of the values, as well as
3051364750dSJames Henderson            # 'interestingness' which is whether any of these values are
3061364750dSJames Henderson            # non-zero.
307*f98ee40fSTobias Hieta            pass_summary_list[i] += (
308*f98ee40fSTobias Hieta                sum(item[1]),
309*f98ee40fSTobias Hieta                min(item[1]),
310*f98ee40fSTobias Hieta                max(item[1]),
311*f98ee40fSTobias Hieta                any(item[1]),
312*f98ee40fSTobias Hieta            )
3131364750dSJames Henderson
3141364750dSJames Henderson            # Split the pass name into the basic name and kind.
315*f98ee40fSTobias Hieta            pass_summary_list[i] += tuple(item[0].rsplit(" on ", 1))
3161364750dSJames Henderson
3171364750dSJames Henderson        # Sort the list by the following columns in order of precedence:
3181364750dSJames Henderson        #   - Is interesting (True first)
3191364750dSJames Henderson        #   - Sum (smallest first)
3201364750dSJames Henderson        #   - Number of times pass ran (largest first)
3211364750dSJames Henderson        #   - Kind (alphabetically)
3221364750dSJames Henderson        #   - Name (alphabetically)
3231364750dSJames Henderson        pass_summary_list.sort(
324*f98ee40fSTobias Hieta            key=lambda tup: (not tup[5], tup[2], -len(tup[1]), tup[7], tup[6])
325*f98ee40fSTobias Hieta        )
3261364750dSJames Henderson
327*f98ee40fSTobias Hieta        with open(path, mode="w", newline="") as fp:
328*f98ee40fSTobias Hieta            writer = csv.writer(fp, delimiter=",")
329*f98ee40fSTobias Hieta            writer.writerow(["Pass", "Kind", "Sum", "Min", "Max", "Interesting"])
3301364750dSJames Henderson
331*f98ee40fSTobias Hieta            for (
332*f98ee40fSTobias Hieta                _,
333*f98ee40fSTobias Hieta                vals,
334*f98ee40fSTobias Hieta                sum_,
335*f98ee40fSTobias Hieta                min_,
336*f98ee40fSTobias Hieta                max_,
337*f98ee40fSTobias Hieta                interesting,
338*f98ee40fSTobias Hieta                name,
339*f98ee40fSTobias Hieta                kind,
340*f98ee40fSTobias Hieta            ) in pass_summary_list:
341*f98ee40fSTobias Hieta                writer.writerow([name, kind, sum_, min_, max_, interesting] + vals)
3421364750dSJames Henderson
3431364750dSJames Henderson        self.context.o.blue('wrote "{}"\n'.format(path))
344