• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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