blob: c5971e6a41c1faee49289c754ba139a7367f805f [file] [log] [blame]
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -07001#!/usr/bin/env python3.4
2#
3# Copyright (C) 2016 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Performs bisection bug search on methods and optimizations.
18
19See README.md.
20
21Example usage:
22./bisection-search.py -cp classes.dex --expected-output output Test
23"""
24
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070025import abc
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070026import argparse
Aart Bike0347482016-09-20 14:34:13 -070027import os
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070028import re
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070029import shlex
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070030import sys
Aart Bike0347482016-09-20 14:34:13 -070031
32from subprocess import call
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070033from tempfile import NamedTemporaryFile
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070034
Aart Bike0347482016-09-20 14:34:13 -070035sys.path.append(os.path.dirname(os.path.dirname(
36 os.path.realpath(__file__))))
37
38from common.common import DeviceTestEnv
39from common.common import FatalError
40from common.common import GetEnvVariableOrError
41from common.common import HostTestEnv
42from common.common import LogSeverity
43from common.common import RetCode
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070044
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070045
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070046# Passes that are never disabled during search process because disabling them
47# would compromise correctness.
48MANDATORY_PASSES = ['dex_cache_array_fixups_arm',
49 'dex_cache_array_fixups_mips',
50 'instruction_simplifier$before_codegen',
51 'pc_relative_fixups_x86',
52 'pc_relative_fixups_mips',
53 'x86_memory_operand_generation']
54
55# Passes that show up as optimizations in compiler verbose output but aren't
56# driven by run-passes mechanism. They are mandatory and will always run, we
57# never pass them to --run-passes.
58NON_PASSES = ['builder', 'prepare_for_register_allocation',
59 'liveness', 'register']
60
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -070061# If present in raw cmd, this tag will be replaced with runtime arguments
62# controlling the bisection search. Otherwise arguments will be placed on second
63# position in the command.
64RAW_CMD_RUNTIME_ARGS_TAG = '{ARGS}'
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070065
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -070066# Default core image path relative to ANDROID_HOST_OUT.
67DEFAULT_IMAGE_RELATIVE_PATH = '/framework/core.art'
68
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070069class Dex2OatWrapperTestable(object):
70 """Class representing a testable compilation.
71
72 Accepts filters on compiled methods and optimization passes.
73 """
74
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -070075 def __init__(self, base_cmd, test_env, expected_retcode=None,
76 output_checker=None, verbose=False):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070077 """Constructor.
78
79 Args:
80 base_cmd: list of strings, base command to run.
81 test_env: ITestEnv.
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -070082 expected_retcode: RetCode, expected normalized return code.
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070083 output_checker: IOutputCheck, output checker.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070084 verbose: bool, enable verbose output.
85 """
86 self._base_cmd = base_cmd
87 self._test_env = test_env
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -070088 self._expected_retcode = expected_retcode
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070089 self._output_checker = output_checker
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070090 self._compiled_methods_path = self._test_env.CreateFile('compiled_methods')
91 self._passes_to_run_path = self._test_env.CreateFile('run_passes')
92 self._verbose = verbose
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -070093 if RAW_CMD_RUNTIME_ARGS_TAG in self._base_cmd:
94 self._arguments_position = self._base_cmd.index(RAW_CMD_RUNTIME_ARGS_TAG)
95 self._base_cmd.pop(self._arguments_position)
96 else:
97 self._arguments_position = 1
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070098
99 def Test(self, compiled_methods, passes_to_run=None):
100 """Tests compilation with compiled_methods and run_passes switches active.
101
102 If compiled_methods is None then compiles all methods.
103 If passes_to_run is None then runs default passes.
104
105 Args:
106 compiled_methods: list of strings representing methods to compile or None.
107 passes_to_run: list of strings representing passes to run or None.
108
109 Returns:
110 True if test passes with given settings. False otherwise.
111 """
112 if self._verbose:
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700113 print('Testing methods: {0} passes: {1}.'.format(
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700114 compiled_methods, passes_to_run))
115 cmd = self._PrepareCmd(compiled_methods=compiled_methods,
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700116 passes_to_run=passes_to_run)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700117 (output, ret_code) = self._test_env.RunCommand(
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700118 cmd, LogSeverity.ERROR)
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700119 res = True
120 if self._expected_retcode:
121 res = self._expected_retcode == ret_code
122 if self._output_checker:
123 res = res and self._output_checker.Check(output)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700124 if self._verbose:
125 print('Test passed: {0}.'.format(res))
126 return res
127
128 def GetAllMethods(self):
129 """Get methods compiled during the test.
130
131 Returns:
132 List of strings representing methods compiled during the test.
133
134 Raises:
135 FatalError: An error occurred when retrieving methods list.
136 """
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700137 cmd = self._PrepareCmd()
138 (output, _) = self._test_env.RunCommand(cmd, LogSeverity.INFO)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700139 match_methods = re.findall(r'Building ([^\n]+)\n', output)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700140 if not match_methods:
141 raise FatalError('Failed to retrieve methods list. '
142 'Not recognized output format.')
143 return match_methods
144
145 def GetAllPassesForMethod(self, compiled_method):
146 """Get all optimization passes ran for a method during the test.
147
148 Args:
149 compiled_method: string representing method to compile.
150
151 Returns:
152 List of strings representing passes ran for compiled_method during test.
153
154 Raises:
155 FatalError: An error occurred when retrieving passes list.
156 """
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700157 cmd = self._PrepareCmd(compiled_methods=[compiled_method])
158 (output, _) = self._test_env.RunCommand(cmd, LogSeverity.INFO)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700159 match_passes = re.findall(r'Starting pass: ([^\n]+)\n', output)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700160 if not match_passes:
161 raise FatalError('Failed to retrieve passes list. '
162 'Not recognized output format.')
163 return [p for p in match_passes if p not in NON_PASSES]
164
Aart Bike0347482016-09-20 14:34:13 -0700165 def _PrepareCmd(self, compiled_methods=None, passes_to_run=None):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700166 """Prepare command to run."""
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700167 cmd = self._base_cmd[0:self._arguments_position]
168 # insert additional arguments before the first argument
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700169 if compiled_methods is not None:
170 self._test_env.WriteLines(self._compiled_methods_path, compiled_methods)
171 cmd += ['-Xcompiler-option', '--compiled-methods={0}'.format(
172 self._compiled_methods_path)]
173 if passes_to_run is not None:
174 self._test_env.WriteLines(self._passes_to_run_path, passes_to_run)
175 cmd += ['-Xcompiler-option', '--run-passes={0}'.format(
176 self._passes_to_run_path)]
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700177 cmd += ['-Xcompiler-option', '--runtime-arg', '-Xcompiler-option',
178 '-verbose:compiler', '-Xcompiler-option', '-j1']
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700179 cmd += self._base_cmd[self._arguments_position:]
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700180 return cmd
181
182
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700183class IOutputCheck(object):
184 """Abstract output checking class.
185
186 Checks if output is correct.
187 """
188 __meta_class__ = abc.ABCMeta
189
190 @abc.abstractmethod
191 def Check(self, output):
192 """Check if output is correct.
193
194 Args:
195 output: string, output to check.
196
197 Returns:
198 boolean, True if output is correct, False otherwise.
199 """
200
201
202class EqualsOutputCheck(IOutputCheck):
203 """Concrete output checking class checking for equality to expected output."""
204
205 def __init__(self, expected_output):
206 """Constructor.
207
208 Args:
209 expected_output: string, expected output.
210 """
211 self._expected_output = expected_output
212
213 def Check(self, output):
214 """See base class."""
215 return self._expected_output == output
216
217
218class ExternalScriptOutputCheck(IOutputCheck):
219 """Concrete output checking class calling an external script.
220
221 The script should accept two arguments, path to expected output and path to
222 program output. It should exit with 0 return code if outputs are equivalent
223 and with different return code otherwise.
224 """
225
226 def __init__(self, script_path, expected_output_path, logfile):
227 """Constructor.
228
229 Args:
230 script_path: string, path to checking script.
231 expected_output_path: string, path to file with expected output.
232 logfile: file handle, logfile.
233 """
234 self._script_path = script_path
235 self._expected_output_path = expected_output_path
236 self._logfile = logfile
237
238 def Check(self, output):
239 """See base class."""
240 ret_code = None
241 with NamedTemporaryFile(mode='w', delete=False) as temp_file:
242 temp_file.write(output)
243 temp_file.flush()
244 ret_code = call(
245 [self._script_path, self._expected_output_path, temp_file.name],
246 stdout=self._logfile, stderr=self._logfile, universal_newlines=True)
247 return ret_code == 0
248
249
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700250def BinarySearch(start, end, test):
251 """Binary search integers using test function to guide the process."""
252 while start < end:
253 mid = (start + end) // 2
254 if test(mid):
255 start = mid + 1
256 else:
257 end = mid
258 return start
259
260
261def FilterPasses(passes, cutoff_idx):
262 """Filters passes list according to cutoff_idx but keeps mandatory passes."""
263 return [opt_pass for idx, opt_pass in enumerate(passes)
264 if opt_pass in MANDATORY_PASSES or idx < cutoff_idx]
265
266
267def BugSearch(testable):
268 """Find buggy (method, optimization pass) pair for a given testable.
269
270 Args:
271 testable: Dex2OatWrapperTestable.
272
273 Returns:
274 (string, string) tuple. First element is name of method which when compiled
275 exposes test failure. Second element is name of optimization pass such that
276 for aforementioned method running all passes up to and excluding the pass
277 results in test passing but running all passes up to and including the pass
278 results in test failing.
279
280 (None, None) if test passes when compiling all methods.
281 (string, None) if a method is found which exposes the failure, but the
282 failure happens even when running just mandatory passes.
283
284 Raises:
285 FatalError: Testable fails with no methods compiled.
286 AssertionError: Method failed for all passes when bisecting methods, but
287 passed when bisecting passes. Possible sporadic failure.
288 """
289 all_methods = testable.GetAllMethods()
290 faulty_method_idx = BinarySearch(
291 0,
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700292 len(all_methods) + 1,
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700293 lambda mid: testable.Test(all_methods[0:mid]))
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700294 if faulty_method_idx == len(all_methods) + 1:
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700295 return (None, None)
296 if faulty_method_idx == 0:
297 raise FatalError('Testable fails with no methods compiled. '
298 'Perhaps issue lies outside of compiler.')
299 faulty_method = all_methods[faulty_method_idx - 1]
300 all_passes = testable.GetAllPassesForMethod(faulty_method)
301 faulty_pass_idx = BinarySearch(
302 0,
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700303 len(all_passes) + 1,
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700304 lambda mid: testable.Test([faulty_method],
305 FilterPasses(all_passes, mid)))
306 if faulty_pass_idx == 0:
307 return (faulty_method, None)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700308 assert faulty_pass_idx != len(all_passes) + 1, ('Method must fail for some '
309 'passes.')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700310 faulty_pass = all_passes[faulty_pass_idx - 1]
311 return (faulty_method, faulty_pass)
312
313
314def PrepareParser():
315 """Prepares argument parser."""
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700316 parser = argparse.ArgumentParser(
317 description='Tool for finding compiler bugs. Either --raw-cmd or both '
318 '-cp and --class are required.')
319 command_opts = parser.add_argument_group('dalvikvm command options')
320 command_opts.add_argument('-cp', '--classpath', type=str, help='classpath')
321 command_opts.add_argument('--class', dest='classname', type=str,
322 help='name of main class')
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700323 command_opts.add_argument('--lib', type=str, default='libart.so',
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700324 help='lib to use, default: libart.so')
325 command_opts.add_argument('--dalvikvm-option', dest='dalvikvm_opts',
326 metavar='OPT', nargs='*', default=[],
327 help='additional dalvikvm option')
328 command_opts.add_argument('--arg', dest='test_args', nargs='*', default=[],
329 metavar='ARG', help='argument passed to test')
330 command_opts.add_argument('--image', type=str, help='path to image')
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700331 command_opts.add_argument('--raw-cmd', type=str,
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700332 help='bisect with this command, ignore other '
333 'command options')
334 bisection_opts = parser.add_argument_group('bisection options')
335 bisection_opts.add_argument('--64', dest='x64', action='store_true',
336 default=False, help='x64 mode')
337 bisection_opts.add_argument(
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700338 '--device', action='store_true', default=False, help='run on device')
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700339 bisection_opts.add_argument(
340 '--device-serial', help='device serial number, implies --device')
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700341 bisection_opts.add_argument('--expected-output', type=str,
342 help='file containing expected output')
343 bisection_opts.add_argument(
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700344 '--expected-retcode', type=str, help='expected normalized return code',
345 choices=[RetCode.SUCCESS.name, RetCode.TIMEOUT.name, RetCode.ERROR.name])
346 bisection_opts.add_argument(
347 '--check-script', type=str,
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700348 help='script comparing output and expected output')
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700349 bisection_opts.add_argument(
350 '--logfile', type=str, help='custom logfile location')
351 bisection_opts.add_argument('--cleanup', action='store_true',
352 default=False, help='clean up after bisecting')
353 bisection_opts.add_argument('--timeout', type=int, default=60,
354 help='if timeout seconds pass assume test failed')
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700355 bisection_opts.add_argument('--verbose', action='store_true',
356 default=False, help='enable verbose output')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700357 return parser
358
359
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700360def PrepareBaseCommand(args, classpath):
361 """Prepares base command used to run test."""
362 if args.raw_cmd:
363 return shlex.split(args.raw_cmd)
364 else:
365 base_cmd = ['dalvikvm64'] if args.x64 else ['dalvikvm32']
366 if not args.device:
367 base_cmd += ['-XXlib:{0}'.format(args.lib)]
368 if not args.image:
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700369 image_path = (GetEnvVariableOrError('ANDROID_HOST_OUT') +
370 DEFAULT_IMAGE_RELATIVE_PATH)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700371 else:
372 image_path = args.image
373 base_cmd += ['-Ximage:{0}'.format(image_path)]
374 if args.dalvikvm_opts:
375 base_cmd += args.dalvikvm_opts
376 base_cmd += ['-cp', classpath, args.classname] + args.test_args
377 return base_cmd
378
379
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700380def main():
381 # Parse arguments
382 parser = PrepareParser()
383 args = parser.parse_args()
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700384 if not args.raw_cmd and (not args.classpath or not args.classname):
385 parser.error('Either --raw-cmd or both -cp and --class are required')
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700386 if args.device_serial:
387 args.device = True
388 if args.expected_retcode:
389 args.expected_retcode = RetCode[args.expected_retcode]
390 if not args.expected_retcode and not args.check_script:
391 args.expected_retcode = RetCode.SUCCESS
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700392
393 # Prepare environment
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700394 classpath = args.classpath
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700395 if args.device:
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700396 test_env = DeviceTestEnv(
397 'bisection_search_', args.cleanup, args.logfile, args.timeout,
398 args.device_serial)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700399 if classpath:
400 classpath = test_env.PushClasspath(classpath)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700401 else:
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700402 test_env = HostTestEnv(
403 'bisection_search_', args.cleanup, args.logfile, args.timeout, args.x64)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700404 base_cmd = PrepareBaseCommand(args, classpath)
405 output_checker = None
406 if args.expected_output:
407 if args.check_script:
408 output_checker = ExternalScriptOutputCheck(
409 args.check_script, args.expected_output, test_env.logfile)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700410 else:
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700411 with open(args.expected_output, 'r') as expected_output_file:
412 output_checker = EqualsOutputCheck(expected_output_file.read())
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700413
414 # Perform the search
415 try:
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700416 testable = Dex2OatWrapperTestable(base_cmd, test_env, args.expected_retcode,
417 output_checker, args.verbose)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700418 (method, opt_pass) = BugSearch(testable)
419 except Exception as e:
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700420 print('Error occurred.\nLogfile: {0}'.format(test_env.logfile.name))
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700421 test_env.logfile.write('Exception: {0}\n'.format(e))
422 raise
423
424 # Report results
425 if method is None:
426 print('Couldn\'t find any bugs.')
427 elif opt_pass is None:
428 print('Faulty method: {0}. Fails with just mandatory passes.'.format(
429 method))
430 else:
431 print('Faulty method and pass: {0}, {1}.'.format(method, opt_pass))
432 print('Logfile: {0}'.format(test_env.logfile.name))
433 sys.exit(0)
434
435
436if __name__ == '__main__':
437 main()