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