1#!/usr/bin/env python3 2# -- coding: utf-8 -- 3# Copyright (c) 2024 Huawei Device Co., Ltd. 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import subprocess 17import sys 18import shutil 19import glob 20import re 21import random 22from collections import namedtuple 23 24HELP_TEXT = """ 25Script to help find minimal subsets of compiled methods, inlined methods and compiler passes 26to reproduce compiler bugs. 27 28Usage: 29 Write a shell command or a bash script which launches a test and exits with non-zero code 30 when the test fails. Use `$GEN_OPTIONS` variable in your script/command where 31 compiler options are needed (probably next to other arguments of `ark`/`ark_aot`), 32 and this script will set it to different values and run your script to find minimal subsets 33 of options which lead to failure. 34 Run `./compiler_bisect.py [COMMAND] [ARGS]...` 35 36Example usage: 37 cd $ARK_BUILD 38 $ARK_ROOT/scripts/compiler_bisect.py bin/ark --boot-panda-files=plugins/ets/etsstdlib.abc --load-runtimes=ets \ 39 --compiler-enable-jit=true --no-async-jit=true --compiler-hotness-threshold=0 --compiler-ignore-failures=false \ 40 \$GEN_OPTIONS test.abc ETSGLOBAL::main 41 42 Or put command in `run.sh` and do `./compiler_bisect ./run.sh` 43 44 Note that you need escaped \$GEN_OPTIONS when passing command with arguments, and not escaped 45 $GEN_OPTIONS inside your script 46""" 47 48 49class Colors: 50 OKCYAN = '\033[96m' 51 OKGREEN = '\033[92m' 52 WARNING = '\033[93m' 53 FAIL = '\033[91m' 54 ENDC = '\033[0m' 55 BOLD = '\033[1m' 56 57 58pass_logs = { 59 'lowering': 'lowering', 60 'code-sink': 'code-sink', 61 'balance-expressions': 'balance-expr', 62 'branch-elimination': 'branch-elim', 63 'checks-elimination': 'checks-elim', 64 'deoptimize-elimination': 'deoptimize-elim', 65 'licm': 'licm-opt', 66 'licm-conditions': 'licm-cond-opt', 67 'loop-unswitch': 'loop-unswitch', 68 'loop-idioms': None, 69 'loop-peeling': None, 70 'loop-unroll': None, 71 'lse': 'lse-opt', 72 'cse': 'cse-opt', 73 'vn': 'vn-opt', 74 'memory-coalescing': 'memory-coalescing', 75 'inlining': 'inlining', 76 'if-conversion': 'ifconversion', 77 'adjust-refs': None, 78 'scalar-replacement': 'pea', 79 'simplify-string-builder': 'simplify-sb', 80 'enable-fast-interop': None, 81 'interop-intrinsic-optimization': 'interop-intrinsic-opt', 82 'peepholes': 'peephole', 83 'move-constants': None, 84 'if-merging': 'if-merging', 85 'redundant-loop-elimination': 'rle-opt', 86 'scheduling': 'scheduler', 87 'optimize-memory-barriers': None, 88} 89pass_list = pass_logs.keys() 90 91 92def print_color(color, *args, **kwargs): 93 print(color, end='') 94 print(*args, **kwargs) 95 print(Colors.ENDC, end='') 96 97 98def exit_fail(message='', usage=False): 99 print(Colors.FAIL, end='') 100 if message: 101 print(message) 102 if usage: 103 print(f"Usage: {sys.argv[0]} COMMAND [ARG]..., COMMAND or ARGS should use $GEN_OPTIONS variable.") 104 print(f"{sys.argv[0]} -h for help") 105 exit(1) 106 107 108def get_run_options(compiled_methods, noinline_methods, passes, dump, verbose): 109 if compiled_methods is None: 110 compiler_regex = '.*' 111 else: 112 compiler_regex = '(' + '|'.join(compiled_methods) + ')' 113 inline_exclude = ','.join(noinline_methods) 114 115 if passes is None: 116 passes = pass_list 117 options = [f'--compiler-regex={compiler_regex}', f'--compiler-inlining-blacklist={inline_exclude}'] 118 options += [f'--compiler-{opt}={str(opt in passes).lower()}' for opt in pass_list] 119 if dump or verbose: 120 options.append('--compiler-dump') 121 if verbose: 122 options.append('--compiler-disasm-dump:single-file') 123 options.append('--log-debug=compiler') 124 compiler_log = set(pass_logs.get(opt) for opt in passes) - {None} 125 if any('loop' in opt for opt in passes): 126 compiler_log.add('loop-transform') 127 if compiler_log: 128 options.append('--compiler-log=' + ','.join(compiler_log)) 129 return options 130 131 132# compiled_methods - methods to compile or None if all 133# noinline - methods to exclude in inlining 134# passes - compiler options from `pass_list` to enable or None if all 135# dump - whether to collect compiler dump 136# expect_fail - if not None, print additional info for unexpected run result 137def run(compiled_methods, noinline_methods, passes, dump=False, verbose=False, expect_fail=None): 138 options = get_run_options(compiled_methods, noinline_methods, passes, dump, verbose) 139 options_str = ' '.join(options) 140 if not verbose: 141 print(f"GEN_OPTIONS='{options_str}'") 142 shutil.rmtree('ir_dump', ignore_errors=True) 143 cmd_and_args = [] 144 for arg in sys.argv[1:]: 145 if arg == '$GEN_OPTIONS': 146 cmd_and_args += options 147 else: 148 cmd_and_args.append(arg) 149 150 env = {"GEN_OPTIONS": options_str} 151 if verbose or (expect_fail is not None): 152 env['SHELLOPTS'] = 'xtrace' 153 154 try: 155 res = subprocess.run(cmd_and_args, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 156 except Exception as e: 157 exit_fail(str(e), usage=True) 158 print_color(Colors.WARNING if res.returncode else Colors.OKGREEN, "return value:", res.returncode) 159 failed = res.returncode != 0 160 if (expect_fail is not None) and failed != expect_fail: 161 verbose = True 162 if verbose: 163 print_out(res) 164 print(f"GEN_OPTIONS='{options_str}'") 165 print('Command:', ' '.join(cmd_and_args)) 166 return res 167 168 169class Option: 170 def __init__(self, values): 171 self.all_values = set(values) 172 self.values = list(values) 173 174 @property 175 def neg_values(self): 176 return self.all_values - set(self.values) 177 178 179class Runner: 180 Options = namedtuple('Options', ['compiled', 'inline', 'passes']) 181 182 def __init__(self, compiled_methods, inline, passes): 183 self.opts = Runner.Options(*map(Option, [compiled_methods, inline, passes])) 184 self.current_option = '' 185 186 def set_option(self, name, value): 187 self.opts = self.opts._replace(**{name: value}) 188 189 def run(self, verbose=False): 190 return run(self.opts.compiled.values, self.opts.inline.neg_values, 191 self.opts.passes.values, verbose=verbose).returncode != 0 192 193 # If script failed, updates the option and returns True; 194 # otherwise rolls the option back and returns False 195 def try_run(self, opt, new_values): 196 print_color(Colors.OKCYAN, f'Try {self.current_option}={new_values}') 197 old_values = opt.values 198 opt.values = new_values 199 if self.run(): 200 return True 201 opt.values = old_values 202 return False 203 204 def bisect(self, option_name): 205 self.current_option = option_name 206 opt = getattr(self.opts, option_name) 207 while True: 208 values = opt.values 209 if len(values) == 0: 210 break 211 if len(values) > 3: 212 mid = len(values) // 2 213 left, right = values[:mid], values[mid:] 214 # Try to reduce option set by half 215 if self.try_run(opt, left): 216 continue 217 if self.try_run(opt, right): 218 continue 219 # If we were unable to reduce option set by half, 220 # try to remove each individual option 221 random.shuffle(values) 222 for i in range(len(values)): 223 new_values = values[:i] + values[i + 1:] 224 if self.try_run(opt, new_values): 225 # continue `while True` 226 break 227 else: 228 # Cannot reduce option set 229 break 230 print_color(Colors.OKGREEN + Colors.BOLD, f'fixed {self.current_option}={opt.values}') 231 232 233def parse_methods(): 234 compiled = set() 235 inlined = set() 236 dumps = glob.glob('ir_dump/*IrBuilder.ir') 237 for dump in dumps: 238 try: 239 method = re.match(r'ir_dump/\d+_pass_\d+_(.*)_IrBuilder.ir', dump).group(1) 240 except Exception: 241 exit_fail('Failed to parse IR dumps') 242 parts = re.split('(?<!_)_(?!_*$)', method) 243 if parts[-1] in ['_ctor_', '_cctor_']: 244 parts[-1] = f'<{parts[-1].strip("_")}>' 245 246 dest = compiled 247 if parts[0] == 'inlined': 248 dest = inlined 249 parts.pop(0) 250 qname = '::'.join(('.'.join(parts[:-1]), parts[-1])) 251 dest.add(qname) 252 return (compiled, inlined) 253 254 255def print_out(res): 256 print('stdout:', res.stdout.decode('unicode_escape'), sep='\n') 257 print('stderr:', res.stderr.decode('unicode_escape'), sep='\n') 258 259 260def main(): 261 if len(sys.argv) < 2: 262 exit_fail(usage=True) 263 if sys.argv[1] in ('-h', '--help'): 264 print(HELP_TEXT) 265 exit(0) 266 267 print_color(Colors.OKCYAN, f'Run without compiled methods') 268 res = run([], [], [], expect_fail=False) 269 if res.returncode: 270 exit_fail("Script failed without compiled methods") 271 272 print_color(Colors.OKCYAN, f'Run with all methods compiled and all optimizations enabled') 273 res = run(None, [], None, dump=True, expect_fail=True) 274 if not res.returncode: 275 exit_fail("Script didn't fail with default args") 276 277 compiled, _ = parse_methods() 278 279 runner = Runner(compiled, [], pass_list) 280 runner.bisect('compiled') 281 282 run(runner.opts.compiled.values, [], None, dump=True) 283 _, inlined = parse_methods() 284 runner.set_option('inline', Option(inlined)) 285 runner.bisect('inline') 286 287 runner.bisect('passes') 288 289 print_color(Colors.OKGREEN + Colors.BOLD, 'Found $GEN_OPTIONS:') 290 print(Colors.BOLD, end='') 291 if runner.run(verbose=True): 292 print_color(Colors.OKGREEN + Colors.BOLD, 'options:', 293 *[f'{name}: {opt.values}' for name, opt in runner.opts._asdict().items()], sep='\n') 294 else: 295 exit_fail("Didn't fail with found options") 296 297 298if __name__ == "__main__": 299 main() 300