aboutsummaryrefslogtreecommitdiff
path: root/binary_search_tool/binary_search_state.py
diff options
context:
space:
mode:
Diffstat (limited to 'binary_search_tool/binary_search_state.py')
-rwxr-xr-xbinary_search_tool/binary_search_state.py1780
1 files changed, 950 insertions, 830 deletions
diff --git a/binary_search_tool/binary_search_state.py b/binary_search_tool/binary_search_state.py
index 1ddd65ce..1b423b5c 100755
--- a/binary_search_tool/binary_search_state.py
+++ b/binary_search_tool/binary_search_state.py
@@ -1,13 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Copyright 2020 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""The binary search wrapper."""
-from __future__ import division
-from __future__ import print_function
import argparse
import contextlib
@@ -30,871 +28,993 @@ from binary_search_tool import pass_mapping
from cros_utils import command_executer
from cros_utils import logger
-GOOD_SET_VAR = 'BISECT_GOOD_SET'
-BAD_SET_VAR = 'BISECT_BAD_SET'
-STATE_FILE = '%s.state' % sys.argv[0]
+GOOD_SET_VAR = "BISECT_GOOD_SET"
+BAD_SET_VAR = "BISECT_BAD_SET"
+
+STATE_FILE = "%s.state" % sys.argv[0]
HIDDEN_STATE_FILE = os.path.join(
- os.path.dirname(STATE_FILE), '.%s' % os.path.basename(STATE_FILE))
+ os.path.dirname(STATE_FILE), ".%s" % os.path.basename(STATE_FILE)
+)
@contextlib.contextmanager
def SetFile(env_var, items):
- """Generate set files that can be used by switch/test scripts.
-
- Generate temporary set file (good/bad) holding contents of good/bad items for
- the current binary search iteration. Store the name of each file as an
- environment variable so all child processes can access it.
-
- This function is a contextmanager, meaning it's meant to be used with the
- "with" statement in Python. This is so cleanup and setup happens automatically
- and cleanly. Execution of the outer "with" statement happens at the "yield"
- statement.
+ """Generate set files that can be used by switch/test scripts.
- Args:
- env_var: What environment variable to store the file name in.
- items: What items are in this set.
- """
- with tempfile.NamedTemporaryFile('w', encoding='utf-8') as f:
- os.environ[env_var] = f.name
- f.write('\n'.join(items))
- f.flush()
- yield
+ Generate temporary set file (good/bad) holding contents of good/bad items for
+ the current binary search iteration. Store the name of each file as an
+ environment variable so all child processes can access it.
-
-class BinarySearchState(object):
- """The binary search state class."""
-
- def __init__(self, get_initial_items, switch_to_good, switch_to_bad,
- test_setup_script, test_script, incremental, prune, pass_bisect,
- ir_diff, iterations, prune_iterations, verify, file_args,
- verbose):
- """BinarySearchState constructor, see Run for full args documentation."""
- self.get_initial_items = get_initial_items
- self.switch_to_good = switch_to_good
- self.switch_to_bad = switch_to_bad
- self.test_setup_script = test_setup_script
- self.test_script = test_script
- self.incremental = incremental
- self.prune = prune
- self.pass_bisect = pass_bisect
- self.ir_diff = ir_diff
- self.iterations = iterations
- self.prune_iterations = prune_iterations
- self.verify = verify
- self.file_args = file_args
- self.verbose = verbose
-
- self.l = logger.GetLogger()
- self.ce = command_executer.GetCommandExecuter()
-
- self.resumed = False
- self.prune_cycles = 0
- self.search_cycles = 0
- self.binary_search = None
- self.all_items = None
- self.cmd_script = None
- self.mode = None
- self.PopulateItemsUsingCommand(self.get_initial_items)
- self.currently_good_items = set()
- self.currently_bad_items = set()
- self.found_items = set()
- self.known_good = set()
-
- self.start_time = time.time()
-
- def SwitchToGood(self, item_list):
- """Switch given items to "good" set."""
- if self.incremental:
- self.l.LogOutput(
- 'Incremental set. Wanted to switch %s to good' % str(item_list),
- print_to_console=self.verbose)
- incremental_items = [
- item for item in item_list if item not in self.currently_good_items
- ]
- item_list = incremental_items
- self.l.LogOutput(
- 'Incremental set. Actually switching %s to good' % str(item_list),
- print_to_console=self.verbose)
-
- if not item_list:
- return
-
- self.l.LogOutput(
- 'Switching %s to good' % str(item_list), print_to_console=self.verbose)
- self.RunSwitchScript(self.switch_to_good, item_list)
- self.currently_good_items = self.currently_good_items.union(set(item_list))
- self.currently_bad_items.difference_update(set(item_list))
-
- def SwitchToBad(self, item_list):
- """Switch given items to "bad" set."""
- if self.incremental:
- self.l.LogOutput(
- 'Incremental set. Wanted to switch %s to bad' % str(item_list),
- print_to_console=self.verbose)
- incremental_items = [
- item for item in item_list if item not in self.currently_bad_items
- ]
- item_list = incremental_items
- self.l.LogOutput(
- 'Incremental set. Actually switching %s to bad' % str(item_list),
- print_to_console=self.verbose)
-
- if not item_list:
- return
-
- self.l.LogOutput(
- 'Switching %s to bad' % str(item_list), print_to_console=self.verbose)
- self.RunSwitchScript(self.switch_to_bad, item_list)
- self.currently_bad_items = self.currently_bad_items.union(set(item_list))
- self.currently_good_items.difference_update(set(item_list))
-
- def RunSwitchScript(self, switch_script, item_list):
- """Pass given items to switch script.
+ This function is a contextmanager, meaning it's meant to be used with the
+ "with" statement in Python. This is so cleanup and setup happens automatically
+ and cleanly. Execution of the outer "with" statement happens at the "yield"
+ statement.
Args:
- switch_script: path to switch script
- item_list: list of all items to be switched
+ env_var: What environment variable to store the file name in.
+ items: What items are in this set.
"""
- if self.file_args:
- with tempfile.NamedTemporaryFile('w', encoding='utf-8') as f:
- f.write('\n'.join(item_list))
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8") as f:
+ os.environ[env_var] = f.name
+ f.write("\n".join(items))
f.flush()
- command = '%s %s' % (switch_script, f.name)
- ret, _, _ = self.ce.RunCommandWExceptionCleanup(
- command, print_to_console=self.verbose)
- else:
- command = '%s %s' % (switch_script, ' '.join(item_list))
- try:
- ret, _, _ = self.ce.RunCommandWExceptionCleanup(
- command, print_to_console=self.verbose)
- except OSError as e:
- if e.errno == errno.E2BIG:
- raise RuntimeError('Too many arguments for switch script! Use '
- '--file_args')
- assert ret == 0, 'Switch script %s returned %d' % (switch_script, ret)
-
- def TestScript(self):
- """Run test script and return exit code from script."""
- command = self.test_script
- ret, _, _ = self.ce.RunCommandWExceptionCleanup(command)
- return ret
-
- def TestSetupScript(self):
- """Run test setup script and return exit code from script."""
- if not self.test_setup_script:
- return 0
-
- command = self.test_setup_script
- ret, _, _ = self.ce.RunCommandWExceptionCleanup(command)
- return ret
-
- def GenerateBadCommandScript(self, bad_items):
- """Generate command line script for building bad item."""
- assert not self.prune, 'Prune must be false if pass_bisect is set.'
- assert len(bad_items) == 1, 'Pruning is off, but number of bad ' \
- 'items found was not 1.'
- item = list(bad_items)[0]
- command = '%s %s' % (self.pass_bisect, item)
- ret, _, _ = self.ce.RunCommandWExceptionCleanup(
- command, print_to_console=self.verbose)
- return ret
-
- def DoVerify(self):
- """Verify correctness of test environment.
-
- Verify that a "good" set of items produces a "good" result and that a "bad"
- set of items produces a "bad" result. To be run directly before running
- DoSearch. If verify is False this step is skipped.
- """
- if not self.verify:
- return
-
- self.l.LogOutput('VERIFICATION')
- self.l.LogOutput('Beginning tests to verify good/bad sets\n')
-
- self._OutputProgress('Verifying items from GOOD set\n')
- with SetFile(GOOD_SET_VAR, self.all_items), SetFile(BAD_SET_VAR, []):
- self.l.LogOutput('Resetting all items to good to verify.')
- self.SwitchToGood(self.all_items)
- status = self.TestSetupScript()
- assert status == 0, 'When reset_to_good, test setup should succeed.'
- status = self.TestScript()
- assert status == 0, 'When reset_to_good, status should be 0.'
-
- self._OutputProgress('Verifying items from BAD set\n')
- with SetFile(GOOD_SET_VAR, []), SetFile(BAD_SET_VAR, self.all_items):
- self.l.LogOutput('Resetting all items to bad to verify.')
- self.SwitchToBad(self.all_items)
- status = self.TestSetupScript()
- # The following assumption is not true; a bad image might not
- # successfully push onto a device.
- # assert status == 0, 'When reset_to_bad, test setup should succeed.'
- if status == 0:
- status = self.TestScript()
- assert status == 1, 'When reset_to_bad, status should be 1.'
-
- def DoSearchBadItems(self):
- """Perform full search for bad items.
-
- Perform full search until prune_iterations number of bad items are found.
- """
- while (True and len(self.all_items) > 1 and
- self.prune_cycles < self.prune_iterations):
- terminated = self.DoBinarySearchBadItems()
- self.prune_cycles += 1
- if not terminated:
- break
- # Prune is set.
- prune_index = self.binary_search.current
-
- # If found item is last item, no new items can be found
- if prune_index == len(self.all_items) - 1:
- self.l.LogOutput('First bad item is the last item. Breaking.')
- self.l.LogOutput('Bad items are: %s' % self.all_items[-1])
- self.found_items.add(self.all_items[-1])
- break
-
- # If already seen item we have no new bad items to find, finish up
- if self.all_items[prune_index] in self.found_items:
- self.l.LogOutput(
- 'Found item already found before: %s.' %
- self.all_items[prune_index],
- print_to_console=self.verbose)
- self.l.LogOutput('No more bad items remaining. Done searching.')
- self.l.LogOutput('Bad items are: %s' % ' '.join(self.found_items))
- break
-
- new_all_items = list(self.all_items)
- # Move prune item to the end of the list.
- new_all_items.append(new_all_items.pop(prune_index))
- self.found_items.add(new_all_items[-1])
-
- # Everything below newly found bad item is now known to be a good item.
- # Take these good items out of the equation to save time on the next
- # search. We save these known good items so they are still sent to the
- # switch_to_good script.
- if prune_index:
- self.known_good.update(new_all_items[:prune_index])
- new_all_items = new_all_items[prune_index:]
-
- self.l.LogOutput(
- 'Old list: %s. New list: %s' % (str(self.all_items),
- str(new_all_items)),
- print_to_console=self.verbose)
-
- if not self.prune:
- self.l.LogOutput('Not continuning further, --prune is not set')
- break
- # FIXME: Do we need to Convert the currently good items to bad
- self.PopulateItemsUsingList(new_all_items)
-
- # If pass level bisecting is set, generate a script which contains command
- # line options to rebuild bad item.
- if self.pass_bisect:
- status = self.GenerateBadCommandScript(self.found_items)
- if status == 0:
- self.cmd_script = os.path.join(
- os.path.dirname(self.pass_bisect), 'cmd_script.sh')
- self.l.LogOutput('Command script generated at %s.' % self.cmd_script)
- else:
- raise RuntimeError('Error while generating command script.')
-
- def DoBinarySearchBadItems(self):
- """Perform single iteration of binary search."""
- # If in resume mode don't reset search_cycles
- if not self.resumed:
- self.search_cycles = 0
- else:
- self.resumed = False
-
- terminated = False
- while self.search_cycles < self.iterations and not terminated:
- self.SaveState()
- self.OutputIterationProgressBadItem()
+ yield
- self.search_cycles += 1
- [bad_items, good_items] = self.GetNextItems()
- with SetFile(GOOD_SET_VAR, good_items), SetFile(BAD_SET_VAR, bad_items):
- # TODO: bad_items should come first.
- self.SwitchToGood(good_items)
- self.SwitchToBad(bad_items)
- status = self.TestSetupScript()
- if status == 0:
- status = self.TestScript()
- terminated = self.binary_search.SetStatus(status)
+class BinarySearchState(object):
+ """The binary search state class."""
- if terminated:
- self.l.LogOutput('Terminated!', print_to_console=self.verbose)
- if not terminated:
- self.l.LogOutput('Ran out of iterations searching...')
- self.l.LogOutput(str(self), print_to_console=self.verbose)
- return terminated
+ def __init__(
+ self,
+ get_initial_items,
+ switch_to_good,
+ switch_to_bad,
+ test_setup_script,
+ test_script,
+ incremental,
+ prune,
+ pass_bisect,
+ ir_diff,
+ iterations,
+ prune_iterations,
+ verify,
+ file_args,
+ verbose,
+ ):
+ """BinarySearchState constructor, see Run for full args documentation."""
+ self.get_initial_items = get_initial_items
+ self.switch_to_good = switch_to_good
+ self.switch_to_bad = switch_to_bad
+ self.test_setup_script = test_setup_script
+ self.test_script = test_script
+ self.incremental = incremental
+ self.prune = prune
+ self.pass_bisect = pass_bisect
+ self.ir_diff = ir_diff
+ self.iterations = iterations
+ self.prune_iterations = prune_iterations
+ self.verify = verify
+ self.file_args = file_args
+ self.verbose = verbose
+
+ self.l = logger.GetLogger()
+ self.ce = command_executer.GetCommandExecuter()
+
+ self.resumed = False
+ self.prune_cycles = 0
+ self.search_cycles = 0
+ self.binary_search = None
+ self.all_items = None
+ self.cmd_script = None
+ self.mode = None
+ self.PopulateItemsUsingCommand(self.get_initial_items)
+ self.currently_good_items = set()
+ self.currently_bad_items = set()
+ self.found_items = set()
+ self.known_good = set()
+
+ self.start_time = time.time()
+
+ def SwitchToGood(self, item_list):
+ """Switch given items to "good" set."""
+ if self.incremental:
+ self.l.LogOutput(
+ "Incremental set. Wanted to switch %s to good" % str(item_list),
+ print_to_console=self.verbose,
+ )
+ incremental_items = [
+ item
+ for item in item_list
+ if item not in self.currently_good_items
+ ]
+ item_list = incremental_items
+ self.l.LogOutput(
+ "Incremental set. Actually switching %s to good"
+ % str(item_list),
+ print_to_console=self.verbose,
+ )
+
+ if not item_list:
+ return
- def CollectPassName(self, pass_info):
- """Mapping opt-bisect output of pass info to debugcounter name."""
- self.l.LogOutput('Pass info: %s' % pass_info, print_to_console=self.verbose)
+ self.l.LogOutput(
+ "Switching %s to good" % str(item_list),
+ print_to_console=self.verbose,
+ )
+ self.RunSwitchScript(self.switch_to_good, item_list)
+ self.currently_good_items = self.currently_good_items.union(
+ set(item_list)
+ )
+ self.currently_bad_items.difference_update(set(item_list))
+
+ def SwitchToBad(self, item_list):
+ """Switch given items to "bad" set."""
+ if self.incremental:
+ self.l.LogOutput(
+ "Incremental set. Wanted to switch %s to bad" % str(item_list),
+ print_to_console=self.verbose,
+ )
+ incremental_items = [
+ item
+ for item in item_list
+ if item not in self.currently_bad_items
+ ]
+ item_list = incremental_items
+ self.l.LogOutput(
+ "Incremental set. Actually switching %s to bad"
+ % str(item_list),
+ print_to_console=self.verbose,
+ )
+
+ if not item_list:
+ return
- for desc in pass_mapping.pass_name:
- if desc in pass_info:
- return pass_mapping.pass_name[desc]
+ self.l.LogOutput(
+ "Switching %s to bad" % str(item_list),
+ print_to_console=self.verbose,
+ )
+ self.RunSwitchScript(self.switch_to_bad, item_list)
+ self.currently_bad_items = self.currently_bad_items.union(
+ set(item_list)
+ )
+ self.currently_good_items.difference_update(set(item_list))
+
+ def RunSwitchScript(self, switch_script, item_list):
+ """Pass given items to switch script.
+
+ Args:
+ switch_script: path to switch script
+ item_list: list of all items to be switched
+ """
+ if self.file_args:
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8") as f:
+ f.write("\n".join(item_list))
+ f.flush()
+ command = "%s %s" % (switch_script, f.name)
+ ret, _, _ = self.ce.RunCommandWExceptionCleanup(
+ command, print_to_console=self.verbose
+ )
+ else:
+ command = "%s %s" % (switch_script, " ".join(item_list))
+ try:
+ ret, _, _ = self.ce.RunCommandWExceptionCleanup(
+ command, print_to_console=self.verbose
+ )
+ except OSError as e:
+ if e.errno == errno.E2BIG:
+ raise RuntimeError(
+ "Too many arguments for switch script! Use "
+ "--file_args"
+ )
+ assert ret == 0, "Switch script %s returned %d" % (switch_script, ret)
+
+ def TestScript(self):
+ """Run test script and return exit code from script."""
+ command = self.test_script
+ ret, _, _ = self.ce.RunCommandWExceptionCleanup(command)
+ return ret
+
+ def TestSetupScript(self):
+ """Run test setup script and return exit code from script."""
+ if not self.test_setup_script:
+ return 0
+
+ command = self.test_setup_script
+ ret, _, _ = self.ce.RunCommandWExceptionCleanup(command)
+ return ret
+
+ def GenerateBadCommandScript(self, bad_items):
+ """Generate command line script for building bad item."""
+ assert not self.prune, "Prune must be false if pass_bisect is set."
+ assert len(bad_items) == 1, (
+ "Pruning is off, but number of bad " "items found was not 1."
+ )
+ item = list(bad_items)[0]
+ command = "%s %s" % (self.pass_bisect, item)
+ ret, _, _ = self.ce.RunCommandWExceptionCleanup(
+ command, print_to_console=self.verbose
+ )
+ return ret
+
+ def DoVerify(self):
+ """Verify correctness of test environment.
+
+ Verify that a "good" set of items produces a "good" result and that a "bad"
+ set of items produces a "bad" result. To be run directly before running
+ DoSearch. If verify is False this step is skipped.
+ """
+ if not self.verify:
+ return
+
+ self.l.LogOutput("VERIFICATION")
+ self.l.LogOutput("Beginning tests to verify good/bad sets\n")
+
+ self._OutputProgress("Verifying items from GOOD set\n")
+ with SetFile(GOOD_SET_VAR, self.all_items), SetFile(BAD_SET_VAR, []):
+ self.l.LogOutput("Resetting all items to good to verify.")
+ self.SwitchToGood(self.all_items)
+ status = self.TestSetupScript()
+ assert status == 0, "When reset_to_good, test setup should succeed."
+ status = self.TestScript()
+ assert status == 0, "When reset_to_good, status should be 0."
+
+ self._OutputProgress("Verifying items from BAD set\n")
+ with SetFile(GOOD_SET_VAR, []), SetFile(BAD_SET_VAR, self.all_items):
+ self.l.LogOutput("Resetting all items to bad to verify.")
+ self.SwitchToBad(self.all_items)
+ status = self.TestSetupScript()
+ # The following assumption is not true; a bad image might not
+ # successfully push onto a device.
+ # assert status == 0, 'When reset_to_bad, test setup should succeed.'
+ if status == 0:
+ status = self.TestScript()
+ assert status == 1, "When reset_to_bad, status should be 1."
+
+ def DoSearchBadItems(self):
+ """Perform full search for bad items.
+
+ Perform full search until prune_iterations number of bad items are found.
+ """
+ while (
+ True
+ and len(self.all_items) > 1
+ and self.prune_cycles < self.prune_iterations
+ ):
+ terminated = self.DoBinarySearchBadItems()
+ self.prune_cycles += 1
+ if not terminated:
+ break
+ # Prune is set.
+ prune_index = self.binary_search.current
+
+ # If found item is last item, no new items can be found
+ if prune_index == len(self.all_items) - 1:
+ self.l.LogOutput("First bad item is the last item. Breaking.")
+ self.l.LogOutput("Bad items are: %s" % self.all_items[-1])
+ self.found_items.add(self.all_items[-1])
+ break
+
+ # If already seen item we have no new bad items to find, finish up
+ if self.all_items[prune_index] in self.found_items:
+ self.l.LogOutput(
+ "Found item already found before: %s."
+ % self.all_items[prune_index],
+ print_to_console=self.verbose,
+ )
+ self.l.LogOutput("No more bad items remaining. Done searching.")
+ self.l.LogOutput(
+ "Bad items are: %s" % " ".join(self.found_items)
+ )
+ break
+
+ new_all_items = list(self.all_items)
+ # Move prune item to the end of the list.
+ new_all_items.append(new_all_items.pop(prune_index))
+ self.found_items.add(new_all_items[-1])
+
+ # Everything below newly found bad item is now known to be a good item.
+ # Take these good items out of the equation to save time on the next
+ # search. We save these known good items so they are still sent to the
+ # switch_to_good script.
+ if prune_index:
+ self.known_good.update(new_all_items[:prune_index])
+ new_all_items = new_all_items[prune_index:]
+
+ self.l.LogOutput(
+ "Old list: %s. New list: %s"
+ % (str(self.all_items), str(new_all_items)),
+ print_to_console=self.verbose,
+ )
+
+ if not self.prune:
+ self.l.LogOutput("Not continuning further, --prune is not set")
+ break
+ # FIXME: Do we need to Convert the currently good items to bad
+ self.PopulateItemsUsingList(new_all_items)
+
+ # If pass level bisecting is set, generate a script which contains command
+ # line options to rebuild bad item.
+ if self.pass_bisect:
+ status = self.GenerateBadCommandScript(self.found_items)
+ if status == 0:
+ self.cmd_script = os.path.join(
+ os.path.dirname(self.pass_bisect), "cmd_script.sh"
+ )
+ self.l.LogOutput(
+ "Command script generated at %s." % self.cmd_script
+ )
+ else:
+ raise RuntimeError("Error while generating command script.")
+
+ def DoBinarySearchBadItems(self):
+ """Perform single iteration of binary search."""
+ # If in resume mode don't reset search_cycles
+ if not self.resumed:
+ self.search_cycles = 0
+ else:
+ self.resumed = False
+
+ terminated = False
+ while self.search_cycles < self.iterations and not terminated:
+ self.SaveState()
+ self.OutputIterationProgressBadItem()
+
+ self.search_cycles += 1
+ [bad_items, good_items] = self.GetNextItems()
+
+ with SetFile(GOOD_SET_VAR, good_items), SetFile(
+ BAD_SET_VAR, bad_items
+ ):
+ # TODO: bad_items should come first.
+ self.SwitchToGood(good_items)
+ self.SwitchToBad(bad_items)
+ status = self.TestSetupScript()
+ if status == 0:
+ status = self.TestScript()
+ terminated = self.binary_search.SetStatus(status)
+
+ if terminated:
+ self.l.LogOutput("Terminated!", print_to_console=self.verbose)
+ if not terminated:
+ self.l.LogOutput("Ran out of iterations searching...")
+ self.l.LogOutput(str(self), print_to_console=self.verbose)
+ return terminated
+
+ def CollectPassName(self, pass_info):
+ """Mapping opt-bisect output of pass info to debugcounter name."""
+ self.l.LogOutput(
+ "Pass info: %s" % pass_info, print_to_console=self.verbose
+ )
+
+ for desc in pass_mapping.pass_name:
+ if desc in pass_info:
+ return pass_mapping.pass_name[desc]
+
+ # If pass not found, return None
+ return None
+
+ def BuildWithPassLimit(self, limit, generate_ir=False):
+ """Rebuild bad item with pass level bisect limit
+
+ Run command line script generated by GenerateBadCommandScript(), with
+ pass level limit flags.
+
+ Returns:
+ pass_num: current number of the pass, or total number of passes if
+ limit set to -1.
+ pass_name: The debugcounter name of current limit pass.
+ """
+ os.environ["LIMIT_FLAGS"] = "-mllvm -opt-bisect-limit=" + str(limit)
+ if generate_ir:
+ os.environ["LIMIT_FLAGS"] += " -S -emit-llvm"
+ self.l.LogOutput(
+ "Limit flags: %s" % os.environ["LIMIT_FLAGS"],
+ print_to_console=self.verbose,
+ )
+ command = self.cmd_script
+ _, _, msg = self.ce.RunCommandWOutput(command, print_to_console=False)
+
+ # Massages we get will be like this:
+ # BISECT: running pass (9) <Pass Description> on <function> (<file>)
+ # BISECT: running pass (10) <Pass Description> on <module> (<file>)
+ # BISECT: NOT running pass (11) <Pass Description> on <SCG> (<file>)
+ # BISECT: NOT running pass (12) <Pass Description> on <SCG> (<file>)
+ # We want to get the pass description of last running pass, to have
+ # transformation level bisect on it.
+ if "BISECT: " not in msg:
+ raise RuntimeError(
+ "No bisect info printed, OptBisect may not be "
+ "supported by the compiler."
+ )
+
+ lines = msg.split("\n")
+ pass_num = 0
+ last_pass = ""
+ for l in lines:
+ if "running pass" in l:
+ # For situation of limit==-1, we want the total number of passes
+ if limit != -1 and "BISECT: NOT " in l:
+ break
+ pass_num += 1
+ last_pass = l
+ if limit not in (-1, pass_num):
+ raise ValueError(
+ "[Error] While building, limit number does not match."
+ )
+ return pass_num, self.CollectPassName(last_pass)
+
+ def BuildWithTransformLimit(
+ self, limit, pass_name=None, pass_limit=-1, generate_ir=False
+ ):
+ """Rebuild bad item with transformation level bisect limit
+
+ Run command line script generated by GenerateBadCommandScript(), with
+ pass level limit flags and transformation level limit flags.
+
+ Args:
+ limit: transformation level limit for bad item.
+ pass_name: name of bad pass debugcounter from pass level bisect result.
+ pass_limit: pass level limit from pass level bisect result.
+ generate_ir: Whether to generate IR comparison.
+
+ Returns:
+ Total number of transformations if limit set to -1, else return 0.
+ """
+ counter_name = pass_name
+
+ os.environ["LIMIT_FLAGS"] = (
+ "-mllvm -opt-bisect-limit="
+ + str(pass_limit)
+ + " -mllvm -debug-counter="
+ + counter_name
+ + "-count="
+ + str(limit)
+ + " -mllvm -print-debug-counter"
+ )
+ if generate_ir:
+ os.environ["LIMIT_FLAGS"] += " -S -emit-llvm"
+ self.l.LogOutput(
+ "Limit flags: %s" % os.environ["LIMIT_FLAGS"],
+ print_to_console=self.verbose,
+ )
+ command = self.cmd_script
+ _, _, msg = self.ce.RunCommandWOutput(command, print_to_console=False)
+
+ if "Counters and values:" not in msg:
+ # Print pass level IR diff only if transformation level bisection does
+ # not work.
+ if self.ir_diff:
+ self.PrintIRDiff(pass_limit)
+ raise RuntimeError(
+ "No bisect info printed, DebugCounter may not be "
+ "supported by the compiler."
+ )
+
+ # With debugcounter enabled, there will be DebugCounter counting info in
+ # the output.
+ lines = msg.split("\n")
+ for l in lines:
+ if pass_name in l:
+ # Output of debugcounter will be like:
+ # instcombine-visit: {10, 0, 20}
+ # dce-transform: {1, 0, -1}
+ # which indicates {Count, Skip, StopAfter}.
+ # The last number should be the limit we set.
+ # We want the first number as the total transformation count.
+ # Split each line by ,|{|} and we can get l_list as:
+ # ['instcombine: ', '10', '0', '20', '']
+ # and we will need the second item in it.
+ l_list = re.split(",|{|}", l)
+ count = int(l_list[1])
+ if limit == -1:
+ return count
+ # The returned value is only useful when limit == -1, which shows total
+ # transformation count.
+ return 0
+
+ def PrintIRDiff(self, pass_index, pass_name=None, trans_index=-1):
+ bad_item = list(self.found_items)[0]
+ self.l.LogOutput(
+ "IR difference before and after bad pass/transformation:",
+ print_to_console=self.verbose,
+ )
+
+ if trans_index == -1:
+ # Pass level IR diff
+ self.BuildWithPassLimit(pass_index, self.ir_diff)
+ good_ir = os.path.join(tempfile.tempdir, "good.s")
+ shutil.copyfile(bad_item, good_ir)
+ pass_index += 1
+ self.BuildWithPassLimit(pass_index, self.ir_diff)
+ else:
+ # Transformation level IR diff
+ self.BuildWithTransformLimit(
+ trans_index, pass_name, pass_index, self.ir_diff
+ )
+ good_ir = os.path.join(tempfile.tempdir, "good.s")
+ shutil.copyfile(bad_item, good_ir)
+ trans_index += 1
+ self.BuildWithTransformLimit(
+ trans_index, pass_name, pass_index, self.ir_diff
+ )
+
+ bad_ir = os.path.join(tempfile.tempdir, "bad.s")
+ shutil.copyfile(bad_item, bad_ir)
+
+ command = "diff %s %s" % (good_ir, bad_ir)
+ _, _, _ = self.ce.RunCommandWOutput(
+ command, print_to_console=self.verbose
+ )
+
+ def DoSearchBadPass(self):
+ """Perform full search for bad pass of bad item."""
+ logger.GetLogger().LogOutput(
+ "Starting to bisect bad pass for bad item."
+ )
+
+ # Pass level bisection
+ self.mode = "pass"
+ self.binary_search = binary_search_perforce.BinarySearcherForPass(
+ logger_to_set=self.l
+ )
+ self.binary_search.total, _ = self.BuildWithPassLimit(-1)
+ logger.GetLogger().LogOutput(
+ "Total %s number: %d" % (self.mode, self.binary_search.total)
+ )
+
+ pass_index, pass_name = self.DoBinarySearchBadPass()
+
+ if not pass_name and pass_index == 0:
+ raise ValueError("Bisecting passes cannot reproduce good result.")
+ logger.GetLogger().LogOutput("Bad pass found: %s." % pass_name)
+
+ # Transformation level bisection.
+ logger.GetLogger().LogOutput(
+ "Starting to bisect at transformation level."
+ )
+
+ self.mode = "transform"
+ self.binary_search = binary_search_perforce.BinarySearcherForPass(
+ logger_to_set=self.l
+ )
+ self.binary_search.total = self.BuildWithTransformLimit(
+ -1, pass_name, pass_index
+ )
+ logger.GetLogger().LogOutput(
+ "Total %s number: %d" % (self.mode, self.binary_search.total)
+ )
+
+ trans_index, _ = self.DoBinarySearchBadPass(pass_index, pass_name)
+ if trans_index == 0:
+ raise ValueError(
+ "Bisecting %s cannot reproduce good result." % pass_name
+ )
+
+ if self.ir_diff:
+ self.PrintIRDiff(pass_index, pass_name, trans_index)
+
+ logger.GetLogger().LogOutput(
+ "Bisection result for bad item %s:\n"
+ "Bad pass: %s at number %d\n"
+ "Bad transformation number: %d"
+ % (self.found_items, pass_name, pass_index, trans_index)
+ )
+
+ def DoBinarySearchBadPass(self, pass_index=-1, pass_name=None):
+ """Perform single iteration of binary search at pass level
+
+ Args:
+ pass_index: Works for transformation level bisection, indicates the limit
+ number of pass from pass level bisecting result.
+ pass_name: Works for transformation level bisection, indicates
+ DebugCounter name of the bad pass from pass level bisecting
+ result.
+
+ Returns:
+ index: Index of problematic pass/transformation.
+ pass_name: Works for pass level bisection, returns DebugCounter name for
+ bad pass.
+ """
+ # If in resume mode don't reset search_cycles
+ if not self.resumed:
+ self.search_cycles = 0
+ else:
+ self.resumed = False
+
+ terminated = False
+ index = 0
+ while self.search_cycles < self.iterations and not terminated:
+ self.SaveState()
+ self.OutputIterationProgressBadPass()
+
+ self.search_cycles += 1
+ current = self.binary_search.GetNext()
+
+ if self.mode == "pass":
+ index, pass_name = self.BuildWithPassLimit(current)
+ else:
+ self.BuildWithTransformLimit(current, pass_name, pass_index)
+ index = current
+
+ # TODO: Newly generated object should not directly replace original
+ # one, need to put it somewhere and symbol link original one to it.
+ # Will update cmd_script to do it.
+
+ status = self.TestSetupScript()
+ assert status == 0, "Test setup should succeed."
+ status = self.TestScript()
+ terminated = self.binary_search.SetStatus(status)
+
+ if terminated:
+ self.l.LogOutput("Terminated!", print_to_console=self.verbose)
+ if not terminated:
+ self.l.LogOutput("Ran out of iterations searching...")
+ self.l.LogOutput(str(self), print_to_console=self.verbose)
+ return index, pass_name
+
+ def PopulateItemsUsingCommand(self, command):
+ """Update all_items and binary search logic from executable.
+
+ This method is mainly required for enumerating the initial list of items
+ from the get_initial_items script.
+
+ Args:
+ command: path to executable that will enumerate items.
+ """
+ ce = command_executer.GetCommandExecuter()
+ _, out, _ = ce.RunCommandWExceptionCleanup(
+ command, return_output=True, print_to_console=self.verbose
+ )
+ all_items = out.split()
+ self.PopulateItemsUsingList(all_items)
+
+ def PopulateItemsUsingList(self, all_items):
+ """Update all_items and binary searching logic from list.
+
+ Args:
+ all_items: new list of all_items
+ """
+ self.all_items = all_items
+ self.binary_search = binary_search_perforce.BinarySearcher(
+ logger_to_set=self.l
+ )
+ self.binary_search.SetSortedList(self.all_items)
+
+ def SaveState(self):
+ """Save state to STATE_FILE.
+
+ SaveState will create a new unique, hidden state file to hold data from
+ object. Then atomically overwrite the STATE_FILE symlink to point to the
+ new data.
+
+ Raises:
+ OSError if STATE_FILE already exists but is not a symlink.
+ """
+ ce, l = self.ce, self.l
+ self.ce, self.l, self.binary_search.logger = None, None, None
+ old_state = None
+
+ _, path = tempfile.mkstemp(prefix=HIDDEN_STATE_FILE, dir=".")
+ with open(path, "wb") as f:
+ pickle.dump(self, f)
+
+ if os.path.exists(STATE_FILE):
+ if os.path.islink(STATE_FILE):
+ old_state = os.readlink(STATE_FILE)
+ else:
+ raise OSError(
+ (
+ "%s already exists and is not a symlink!\n"
+ "State file saved to %s" % (STATE_FILE, path)
+ )
+ )
+
+ # Create new link and atomically overwrite old link
+ temp_link = "%s.link" % HIDDEN_STATE_FILE
+ os.symlink(path, temp_link)
+ os.rename(temp_link, STATE_FILE)
+
+ if old_state:
+ os.remove(old_state)
+
+ self.ce, self.l, self.binary_search.logger = ce, l, l
+
+ @classmethod
+ def LoadState(cls):
+ """Create BinarySearchState object from STATE_FILE."""
+ if not os.path.isfile(STATE_FILE):
+ return None
+ try:
+ with open(STATE_FILE, "rb") as f:
+ bss = pickle.load(f)
+ bss.l = logger.GetLogger()
+ bss.ce = command_executer.GetCommandExecuter()
+ bss.binary_search.logger = bss.l
+ bss.start_time = time.time()
+
+ # Set resumed to be True so we can enter DoBinarySearch without the
+ # method resetting our current search_cycles to 0.
+ bss.resumed = True
+
+ # Set currently_good_items and currently_bad_items to empty so that the
+ # first iteration after resuming will always be non-incremental. This
+ # is just in case the environment changes, the user makes manual
+ # changes, or a previous switch_script corrupted the environment.
+ bss.currently_good_items = set()
+ bss.currently_bad_items = set()
+
+ binary_search_perforce.verbose = bss.verbose
+ return bss
+ except Exception:
+ return None
+
+ def RemoveState(self):
+ """Remove STATE_FILE and its symlinked data from file system."""
+ if os.path.exists(STATE_FILE):
+ if os.path.islink(STATE_FILE):
+ real_file = os.readlink(STATE_FILE)
+ os.remove(real_file)
+ os.remove(STATE_FILE)
+
+ def GetNextItems(self):
+ """Get next items for binary search based on result of the last test run."""
+ border_item = self.binary_search.GetNext()
+ index = self.all_items.index(border_item)
+
+ next_bad_items = self.all_items[: index + 1]
+ next_good_items = self.all_items[index + 1 :] + list(self.known_good)
+
+ return [next_bad_items, next_good_items]
+
+ def ElapsedTimeString(self):
+ """Return h m s format of elapsed time since execution has started."""
+ diff = int(time.time() - self.start_time)
+ seconds = diff % 60
+ minutes = (diff // 60) % 60
+ hours = diff // (60 * 60)
+
+ seconds = str(seconds).rjust(2)
+ minutes = str(minutes).rjust(2)
+ hours = str(hours).rjust(2)
+
+ return "%sh %sm %ss" % (hours, minutes, seconds)
+
+ def _OutputProgress(self, progress_text):
+ """Output current progress of binary search to console and logs.
+
+ Args:
+ progress_text: The progress to display to the user.
+ """
+ progress = (
+ "\n***** PROGRESS (elapsed time: %s) *****\n"
+ "%s"
+ "************************************************"
+ )
+ progress = progress % (self.ElapsedTimeString(), progress_text)
+ self.l.LogOutput(progress)
+
+ def OutputIterationProgressBadItem(self):
+ out = (
+ "Search %d of estimated %d.\n"
+ "Prune %d of max %d.\n"
+ "Current bad items found:\n"
+ "%s\n"
+ )
+ out = out % (
+ self.search_cycles + 1,
+ math.ceil(math.log(len(self.all_items), 2)),
+ self.prune_cycles + 1,
+ self.prune_iterations,
+ ", ".join(self.found_items),
+ )
+ self._OutputProgress(out)
+
+ def OutputIterationProgressBadPass(self):
+ out = "Search %d of estimated %d.\n" "Current limit: %s\n"
+ out = out % (
+ self.search_cycles + 1,
+ math.ceil(math.log(self.binary_search.total, 2)),
+ self.binary_search.current,
+ )
+ self._OutputProgress(out)
+
+ def __str__(self):
+ ret = ""
+ ret += "all: %s\n" % str(self.all_items)
+ ret += "currently_good: %s\n" % str(self.currently_good_items)
+ ret += "currently_bad: %s\n" % str(self.currently_bad_items)
+ ret += str(self.binary_search)
+ return ret
- # If pass not found, return None
- return None
- def BuildWithPassLimit(self, limit, generate_ir=False):
- """Rebuild bad item with pass level bisect limit
+class MockBinarySearchState(BinarySearchState):
+ """Mock class for BinarySearchState."""
+
+ def __init__(self, **kwargs):
+ # Initialize all arguments to None
+ default_kwargs = {
+ "get_initial_items": 'echo "1"',
+ "switch_to_good": None,
+ "switch_to_bad": None,
+ "test_setup_script": None,
+ "test_script": None,
+ "incremental": True,
+ "prune": False,
+ "pass_bisect": None,
+ "ir_diff": False,
+ "iterations": 50,
+ "prune_iterations": 100,
+ "verify": True,
+ "file_args": False,
+ "verbose": False,
+ }
+ default_kwargs.update(kwargs)
+ super(MockBinarySearchState, self).__init__(**default_kwargs)
- Run command line script generated by GenerateBadCommandScript(), with
- pass level limit flags.
- Returns:
- pass_num: current number of the pass, or total number of passes if
- limit set to -1.
- pass_name: The debugcounter name of current limit pass.
- """
- os.environ['LIMIT_FLAGS'] = '-mllvm -opt-bisect-limit=' + str(limit)
- if generate_ir:
- os.environ['LIMIT_FLAGS'] += ' -S -emit-llvm'
- self.l.LogOutput(
- 'Limit flags: %s' % os.environ['LIMIT_FLAGS'],
- print_to_console=self.verbose)
- command = self.cmd_script
- _, _, msg = self.ce.RunCommandWOutput(command, print_to_console=False)
-
- # Massages we get will be like this:
- # BISECT: running pass (9) <Pass Description> on <function> (<file>)
- # BISECT: running pass (10) <Pass Description> on <module> (<file>)
- # BISECT: NOT running pass (11) <Pass Description> on <SCG> (<file>)
- # BISECT: NOT running pass (12) <Pass Description> on <SCG> (<file>)
- # We want to get the pass description of last running pass, to have
- # transformation level bisect on it.
- if 'BISECT: ' not in msg:
- raise RuntimeError('No bisect info printed, OptBisect may not be '
- 'supported by the compiler.')
-
- lines = msg.split('\n')
- pass_num = 0
- last_pass = ''
- for l in lines:
- if 'running pass' in l:
- # For situation of limit==-1, we want the total number of passes
- if limit != -1 and 'BISECT: NOT ' in l:
- break
- pass_num += 1
- last_pass = l
- if limit not in (-1, pass_num):
- raise ValueError('[Error] While building, limit number does not match.')
- return pass_num, self.CollectPassName(last_pass)
-
- def BuildWithTransformLimit(self,
- limit,
- pass_name=None,
- pass_limit=-1,
- generate_ir=False):
- """Rebuild bad item with transformation level bisect limit
-
- Run command line script generated by GenerateBadCommandScript(), with
- pass level limit flags and transformation level limit flags.
+def _CanonicalizeScript(script_name):
+ """Return canonical path to script.
Args:
- limit: transformation level limit for bad item.
- pass_name: name of bad pass debugcounter from pass level bisect result.
- pass_limit: pass level limit from pass level bisect result.
- generate_ir: Whether to generate IR comparison.
+ script_name: Relative or absolute path to script
Returns:
- Total number of transformations if limit set to -1, else return 0.
+ Canonicalized script path
"""
- counter_name = pass_name
-
- os.environ['LIMIT_FLAGS'] = '-mllvm -opt-bisect-limit=' + \
- str(pass_limit) + \
- ' -mllvm -debug-counter=' + counter_name + \
- '-count=' + str(limit) + \
- ' -mllvm -print-debug-counter'
- if generate_ir:
- os.environ['LIMIT_FLAGS'] += ' -S -emit-llvm'
- self.l.LogOutput(
- 'Limit flags: %s' % os.environ['LIMIT_FLAGS'],
- print_to_console=self.verbose)
- command = self.cmd_script
- _, _, msg = self.ce.RunCommandWOutput(command, print_to_console=False)
-
- if 'Counters and values:' not in msg:
- # Print pass level IR diff only if transformation level bisection does
- # not work.
- if self.ir_diff:
- self.PrintIRDiff(pass_limit)
- raise RuntimeError('No bisect info printed, DebugCounter may not be '
- 'supported by the compiler.')
-
- # With debugcounter enabled, there will be DebugCounter counting info in
- # the output.
- lines = msg.split('\n')
- for l in lines:
- if pass_name in l:
- # Output of debugcounter will be like:
- # instcombine-visit: {10, 0, 20}
- # dce-transform: {1, 0, -1}
- # which indicates {Count, Skip, StopAfter}.
- # The last number should be the limit we set.
- # We want the first number as the total transformation count.
- # Split each line by ,|{|} and we can get l_list as:
- # ['instcombine: ', '10', '0', '20', '']
- # and we will need the second item in it.
- l_list = re.split(',|{|}', l)
- count = int(l_list[1])
- if limit == -1:
- return count
- # The returned value is only useful when limit == -1, which shows total
- # transformation count.
- return 0
-
- def PrintIRDiff(self, pass_index, pass_name=None, trans_index=-1):
- bad_item = list(self.found_items)[0]
- self.l.LogOutput(
- 'IR difference before and after bad pass/transformation:',
- print_to_console=self.verbose)
-
- if trans_index == -1:
- # Pass level IR diff
- self.BuildWithPassLimit(pass_index, self.ir_diff)
- good_ir = os.path.join(tempfile.tempdir, 'good.s')
- shutil.copyfile(bad_item, good_ir)
- pass_index += 1
- self.BuildWithPassLimit(pass_index, self.ir_diff)
- else:
- # Transformation level IR diff
- self.BuildWithTransformLimit(trans_index, pass_name, pass_index,
- self.ir_diff)
- good_ir = os.path.join(tempfile.tempdir, 'good.s')
- shutil.copyfile(bad_item, good_ir)
- trans_index += 1
- self.BuildWithTransformLimit(trans_index, pass_name, pass_index,
- self.ir_diff)
-
- bad_ir = os.path.join(tempfile.tempdir, 'bad.s')
- shutil.copyfile(bad_item, bad_ir)
-
- command = 'diff %s %s' % (good_ir, bad_ir)
- _, _, _ = self.ce.RunCommandWOutput(command, print_to_console=self.verbose)
-
- def DoSearchBadPass(self):
- """Perform full search for bad pass of bad item."""
- logger.GetLogger().LogOutput('Starting to bisect bad pass for bad item.')
-
- # Pass level bisection
- self.mode = 'pass'
- self.binary_search = binary_search_perforce.BinarySearcherForPass(
- logger_to_set=self.l)
- self.binary_search.total, _ = self.BuildWithPassLimit(-1)
- logger.GetLogger().LogOutput(
- 'Total %s number: %d' % (self.mode, self.binary_search.total))
-
- pass_index, pass_name = self.DoBinarySearchBadPass()
-
- if (not pass_name and pass_index == 0):
- raise ValueError('Bisecting passes cannot reproduce good result.')
- logger.GetLogger().LogOutput('Bad pass found: %s.' % pass_name)
-
- # Transformation level bisection.
- logger.GetLogger().LogOutput('Starting to bisect at transformation level.')
-
- self.mode = 'transform'
- self.binary_search = binary_search_perforce.BinarySearcherForPass(
- logger_to_set=self.l)
- self.binary_search.total = self.BuildWithTransformLimit(
- -1, pass_name, pass_index)
- logger.GetLogger().LogOutput(
- 'Total %s number: %d' % (self.mode, self.binary_search.total))
-
- trans_index, _ = self.DoBinarySearchBadPass(pass_index, pass_name)
- if trans_index == 0:
- raise ValueError('Bisecting %s cannot reproduce good result.' % pass_name)
-
- if self.ir_diff:
- self.PrintIRDiff(pass_index, pass_name, trans_index)
-
- logger.GetLogger().LogOutput(
- 'Bisection result for bad item %s:\n'
- 'Bad pass: %s at number %d\n'
- 'Bad transformation number: %d' % (self.found_items, pass_name,
- pass_index, trans_index))
-
- def DoBinarySearchBadPass(self, pass_index=-1, pass_name=None):
- """Perform single iteration of binary search at pass level
+ script_name = os.path.expanduser(script_name)
+ if not script_name.startswith("/"):
+ return os.path.join(".", script_name)
+
+
+def Run(
+ get_initial_items,
+ switch_to_good,
+ switch_to_bad,
+ test_script,
+ test_setup_script=None,
+ iterations=50,
+ prune=False,
+ pass_bisect=None,
+ ir_diff=False,
+ noincremental=False,
+ file_args=False,
+ verify=True,
+ prune_iterations=100,
+ verbose=False,
+ resume=False,
+):
+ """Run binary search tool.
+
+ Equivalent to running through terminal.
Args:
- pass_index: Works for transformation level bisection, indicates the limit
- number of pass from pass level bisecting result.
- pass_name: Works for transformation level bisection, indicates
- DebugCounter name of the bad pass from pass level bisecting
- result.
+ get_initial_items: Script to enumerate all items being binary searched
+ switch_to_good: Script that will take items as input and switch them to good
+ set
+ switch_to_bad: Script that will take items as input and switch them to bad
+ set
+ test_script: Script that will determine if the current combination of good
+ and bad items make a "good" or "bad" result.
+ test_setup_script: Script to do necessary setup (building, compilation,
+ etc.) for test_script.
+ iterations: How many binary search iterations to run before exiting.
+ prune: If False the binary search tool will stop when the first bad item is
+ found. Otherwise then binary search tool will continue searching until all
+ bad items are found (or prune_iterations is reached).
+ pass_bisect: Script that takes single bad item from POPULATE_BAD and returns
+ the compiler command used to generate the bad item. This will turn on
+ pass/ transformation level bisection for the bad item. Requires that
+ 'prune' be set to False, and needs support of `-opt-bisect-limit`(pass)
+ and `-print-debug-counter`(transformation) from LLVM.
+ ir_diff: Whether to print IR differences before and after bad
+ pass/transformation to verbose output. Defaults to False, only works when
+ pass_bisect is enabled.
+ noincremental: Whether to send "diffs" of good/bad items to switch scripts.
+ file_args: If True then arguments to switch scripts will be a file name
+ containing a newline separated list of the items to switch.
+ verify: If True, run tests to ensure initial good/bad sets actually produce
+ a good/bad result.
+ prune_iterations: Max number of bad items to search for.
+ verbose: If True will print extra debug information to user.
+ resume: If True will resume using STATE_FILE.
Returns:
- index: Index of problematic pass/transformation.
- pass_name: Works for pass level bisection, returns DebugCounter name for
- bad pass.
+ 0 for success, error otherwise
"""
- # If in resume mode don't reset search_cycles
- if not self.resumed:
- self.search_cycles = 0
+ # Notice that all the argument checks are in the Run() function rather than
+ # in the Main() function. It is not common to do so but some wrappers are
+ # going to call Run() directly and bypass checks in Main() function.
+ if resume:
+ logger.GetLogger().LogOutput("Resuming from %s" % STATE_FILE)
+ bss = BinarySearchState.LoadState()
+ if not bss:
+ logger.GetLogger().LogOutput(
+ "%s is not a valid binary_search_tool state file, cannot resume!"
+ % STATE_FILE
+ )
+ return 1
+ logger.GetLogger().LogOutput(
+ "Note: resuming from previous state, "
+ "ignoring given options and loading saved "
+ "options instead."
+ )
else:
- self.resumed = False
-
- terminated = False
- index = 0
- while self.search_cycles < self.iterations and not terminated:
- self.SaveState()
- self.OutputIterationProgressBadPass()
-
- self.search_cycles += 1
- current = self.binary_search.GetNext()
-
- if self.mode == 'pass':
- index, pass_name = self.BuildWithPassLimit(current)
- else:
- self.BuildWithTransformLimit(current, pass_name, pass_index)
- index = current
-
- # TODO: Newly generated object should not directly replace original
- # one, need to put it somewhere and symbol link original one to it.
- # Will update cmd_script to do it.
-
- status = self.TestSetupScript()
- assert status == 0, 'Test setup should succeed.'
- status = self.TestScript()
- terminated = self.binary_search.SetStatus(status)
-
- if terminated:
- self.l.LogOutput('Terminated!', print_to_console=self.verbose)
- if not terminated:
- self.l.LogOutput('Ran out of iterations searching...')
- self.l.LogOutput(str(self), print_to_console=self.verbose)
- return index, pass_name
-
- def PopulateItemsUsingCommand(self, command):
- """Update all_items and binary search logic from executable.
-
- This method is mainly required for enumerating the initial list of items
- from the get_initial_items script.
-
- Args:
- command: path to executable that will enumerate items.
- """
- ce = command_executer.GetCommandExecuter()
- _, out, _ = ce.RunCommandWExceptionCleanup(
- command, return_output=True, print_to_console=self.verbose)
- all_items = out.split()
- self.PopulateItemsUsingList(all_items)
-
- def PopulateItemsUsingList(self, all_items):
- """Update all_items and binary searching logic from list.
-
- Args:
- all_items: new list of all_items
- """
- self.all_items = all_items
- self.binary_search = binary_search_perforce.BinarySearcher(
- logger_to_set=self.l)
- self.binary_search.SetSortedList(self.all_items)
-
- def SaveState(self):
- """Save state to STATE_FILE.
-
- SaveState will create a new unique, hidden state file to hold data from
- object. Then atomically overwrite the STATE_FILE symlink to point to the
- new data.
-
- Raises:
- OSError if STATE_FILE already exists but is not a symlink.
- """
- ce, l = self.ce, self.l
- self.ce, self.l, self.binary_search.logger = None, None, None
- old_state = None
-
- _, path = tempfile.mkstemp(prefix=HIDDEN_STATE_FILE, dir='.')
- with open(path, 'wb') as f:
- pickle.dump(self, f)
-
- if os.path.exists(STATE_FILE):
- if os.path.islink(STATE_FILE):
- old_state = os.readlink(STATE_FILE)
- else:
- raise OSError(('%s already exists and is not a symlink!\n'
- 'State file saved to %s' % (STATE_FILE, path)))
-
- # Create new link and atomically overwrite old link
- temp_link = '%s.link' % HIDDEN_STATE_FILE
- os.symlink(path, temp_link)
- os.rename(temp_link, STATE_FILE)
-
- if old_state:
- os.remove(old_state)
-
- self.ce, self.l, self.binary_search.logger = ce, l, l
-
- @classmethod
- def LoadState(cls):
- """Create BinarySearchState object from STATE_FILE."""
- if not os.path.isfile(STATE_FILE):
- return None
- try:
- with open(STATE_FILE, 'rb') as f:
- bss = pickle.load(f)
- bss.l = logger.GetLogger()
- bss.ce = command_executer.GetCommandExecuter()
- bss.binary_search.logger = bss.l
- bss.start_time = time.time()
-
- # Set resumed to be True so we can enter DoBinarySearch without the
- # method resetting our current search_cycles to 0.
- bss.resumed = True
-
- # Set currently_good_items and currently_bad_items to empty so that the
- # first iteration after resuming will always be non-incremental. This
- # is just in case the environment changes, the user makes manual
- # changes, or a previous switch_script corrupted the environment.
- bss.currently_good_items = set()
- bss.currently_bad_items = set()
-
- binary_search_perforce.verbose = bss.verbose
- return bss
- except Exception:
- return None
-
- def RemoveState(self):
- """Remove STATE_FILE and its symlinked data from file system."""
- if os.path.exists(STATE_FILE):
- if os.path.islink(STATE_FILE):
- real_file = os.readlink(STATE_FILE)
- os.remove(real_file)
- os.remove(STATE_FILE)
-
- def GetNextItems(self):
- """Get next items for binary search based on result of the last test run."""
- border_item = self.binary_search.GetNext()
- index = self.all_items.index(border_item)
-
- next_bad_items = self.all_items[:index + 1]
- next_good_items = self.all_items[index + 1:] + list(self.known_good)
-
- return [next_bad_items, next_good_items]
-
- def ElapsedTimeString(self):
- """Return h m s format of elapsed time since execution has started."""
- diff = int(time.time() - self.start_time)
- seconds = diff % 60
- minutes = (diff // 60) % 60
- hours = diff // (60 * 60)
-
- seconds = str(seconds).rjust(2)
- minutes = str(minutes).rjust(2)
- hours = str(hours).rjust(2)
-
- return '%sh %sm %ss' % (hours, minutes, seconds)
-
- def _OutputProgress(self, progress_text):
- """Output current progress of binary search to console and logs.
-
- Args:
- progress_text: The progress to display to the user.
- """
- progress = ('\n***** PROGRESS (elapsed time: %s) *****\n'
- '%s'
- '************************************************')
- progress = progress % (self.ElapsedTimeString(), progress_text)
- self.l.LogOutput(progress)
-
- def OutputIterationProgressBadItem(self):
- out = ('Search %d of estimated %d.\n'
- 'Prune %d of max %d.\n'
- 'Current bad items found:\n'
- '%s\n')
- out = out % (self.search_cycles + 1,
- math.ceil(math.log(len(self.all_items), 2)), self.prune_cycles
- + 1, self.prune_iterations, ', '.join(self.found_items))
- self._OutputProgress(out)
-
- def OutputIterationProgressBadPass(self):
- out = ('Search %d of estimated %d.\n' 'Current limit: %s\n')
- out = out % (self.search_cycles + 1,
- math.ceil(math.log(self.binary_search.total, 2)),
- self.binary_search.current)
- self._OutputProgress(out)
-
- def __str__(self):
- ret = ''
- ret += 'all: %s\n' % str(self.all_items)
- ret += 'currently_good: %s\n' % str(self.currently_good_items)
- ret += 'currently_bad: %s\n' % str(self.currently_bad_items)
- ret += str(self.binary_search)
- return ret
-
-
-class MockBinarySearchState(BinarySearchState):
- """Mock class for BinarySearchState."""
-
- def __init__(self, **kwargs):
- # Initialize all arguments to None
- default_kwargs = {
- 'get_initial_items': 'echo "1"',
- 'switch_to_good': None,
- 'switch_to_bad': None,
- 'test_setup_script': None,
- 'test_script': None,
- 'incremental': True,
- 'prune': False,
- 'pass_bisect': None,
- 'ir_diff': False,
- 'iterations': 50,
- 'prune_iterations': 100,
- 'verify': True,
- 'file_args': False,
- 'verbose': False
- }
- default_kwargs.update(kwargs)
- super(MockBinarySearchState, self).__init__(**default_kwargs)
-
-
-def _CanonicalizeScript(script_name):
- """Return canonical path to script.
-
- Args:
- script_name: Relative or absolute path to script
-
- Returns:
- Canonicalized script path
- """
- script_name = os.path.expanduser(script_name)
- if not script_name.startswith('/'):
- return os.path.join('.', script_name)
-
-
-def Run(get_initial_items,
- switch_to_good,
- switch_to_bad,
- test_script,
- test_setup_script=None,
- iterations=50,
- prune=False,
- pass_bisect=None,
- ir_diff=False,
- noincremental=False,
- file_args=False,
- verify=True,
- prune_iterations=100,
- verbose=False,
- resume=False):
- """Run binary search tool.
-
- Equivalent to running through terminal.
-
- Args:
- get_initial_items: Script to enumerate all items being binary searched
- switch_to_good: Script that will take items as input and switch them to good
- set
- switch_to_bad: Script that will take items as input and switch them to bad
- set
- test_script: Script that will determine if the current combination of good
- and bad items make a "good" or "bad" result.
- test_setup_script: Script to do necessary setup (building, compilation,
- etc.) for test_script.
- iterations: How many binary search iterations to run before exiting.
- prune: If False the binary search tool will stop when the first bad item is
- found. Otherwise then binary search tool will continue searching until all
- bad items are found (or prune_iterations is reached).
- pass_bisect: Script that takes single bad item from POPULATE_BAD and returns
- the compiler command used to generate the bad item. This will turn on
- pass/ transformation level bisection for the bad item. Requires that
- 'prune' be set to False, and needs support of `-opt-bisect-limit`(pass)
- and `-print-debug-counter`(transformation) from LLVM.
- ir_diff: Whether to print IR differences before and after bad
- pass/transformation to verbose output. Defaults to False, only works when
- pass_bisect is enabled.
- noincremental: Whether to send "diffs" of good/bad items to switch scripts.
- file_args: If True then arguments to switch scripts will be a file name
- containing a newline separated list of the items to switch.
- verify: If True, run tests to ensure initial good/bad sets actually produce
- a good/bad result.
- prune_iterations: Max number of bad items to search for.
- verbose: If True will print extra debug information to user.
- resume: If True will resume using STATE_FILE.
-
- Returns:
- 0 for success, error otherwise
- """
- # Notice that all the argument checks are in the Run() function rather than
- # in the Main() function. It is not common to do so but some wrappers are
- # going to call Run() directly and bypass checks in Main() function.
- if resume:
- logger.GetLogger().LogOutput('Resuming from %s' % STATE_FILE)
- bss = BinarySearchState.LoadState()
- if not bss:
- logger.GetLogger().LogOutput(
- '%s is not a valid binary_search_tool state file, cannot resume!' %
- STATE_FILE)
- return 1
- logger.GetLogger().LogOutput('Note: resuming from previous state, '
- 'ignoring given options and loading saved '
- 'options instead.')
- else:
- if not (get_initial_items and switch_to_good and switch_to_bad and
- test_script):
- logger.GetLogger().LogOutput('The following options are required: '
- '[-i, -g, -b, -t] | [-r]')
- return 1
- if pass_bisect and prune:
- logger.GetLogger().LogOutput('"--pass_bisect" only works when '
- '"--prune" is set to be False.')
- return 1
- if not pass_bisect and ir_diff:
- logger.GetLogger().LogOutput('"--ir_diff" only works when '
- '"--pass_bisect" is enabled.')
-
- switch_to_good = _CanonicalizeScript(switch_to_good)
- switch_to_bad = _CanonicalizeScript(switch_to_bad)
- if test_setup_script:
- test_setup_script = _CanonicalizeScript(test_setup_script)
+ if not (
+ get_initial_items
+ and switch_to_good
+ and switch_to_bad
+ and test_script
+ ):
+ logger.GetLogger().LogOutput(
+ "The following options are required: " "[-i, -g, -b, -t] | [-r]"
+ )
+ return 1
+ if pass_bisect and prune:
+ logger.GetLogger().LogOutput(
+ '"--pass_bisect" only works when '
+ '"--prune" is set to be False.'
+ )
+ return 1
+ if not pass_bisect and ir_diff:
+ logger.GetLogger().LogOutput(
+ '"--ir_diff" only works when ' '"--pass_bisect" is enabled.'
+ )
+
+ switch_to_good = _CanonicalizeScript(switch_to_good)
+ switch_to_bad = _CanonicalizeScript(switch_to_bad)
+ if test_setup_script:
+ test_setup_script = _CanonicalizeScript(test_setup_script)
+ if pass_bisect:
+ pass_bisect = _CanonicalizeScript(pass_bisect)
+ test_script = _CanonicalizeScript(test_script)
+ get_initial_items = _CanonicalizeScript(get_initial_items)
+ incremental = not noincremental
+
+ binary_search_perforce.verbose = verbose
+
+ bss = BinarySearchState(
+ get_initial_items,
+ switch_to_good,
+ switch_to_bad,
+ test_setup_script,
+ test_script,
+ incremental,
+ prune,
+ pass_bisect,
+ ir_diff,
+ iterations,
+ prune_iterations,
+ verify,
+ file_args,
+ verbose,
+ )
+ bss.DoVerify()
+
+ bss.DoSearchBadItems()
if pass_bisect:
- pass_bisect = _CanonicalizeScript(pass_bisect)
- test_script = _CanonicalizeScript(test_script)
- get_initial_items = _CanonicalizeScript(get_initial_items)
- incremental = not noincremental
-
- binary_search_perforce.verbose = verbose
-
- bss = BinarySearchState(get_initial_items, switch_to_good, switch_to_bad,
- test_setup_script, test_script, incremental, prune,
- pass_bisect, ir_diff, iterations, prune_iterations,
- verify, file_args, verbose)
- bss.DoVerify()
-
- bss.DoSearchBadItems()
- if pass_bisect:
- bss.DoSearchBadPass()
- bss.RemoveState()
- logger.GetLogger().LogOutput(
- 'Total execution time: %s' % bss.ElapsedTimeString())
+ bss.DoSearchBadPass()
+ bss.RemoveState()
+ logger.GetLogger().LogOutput(
+ "Total execution time: %s" % bss.ElapsedTimeString()
+ )
- return 0
+ return 0
def Main(argv):
- """The main function."""
- # Common initializations
+ """The main function."""
+ # Common initializations
- parser = argparse.ArgumentParser()
- common.BuildArgParser(parser)
- logger.GetLogger().LogOutput(' '.join(argv))
- options = parser.parse_args(argv)
+ parser = argparse.ArgumentParser()
+ common.BuildArgParser(parser)
+ logger.GetLogger().LogOutput(" ".join(argv))
+ options = parser.parse_args(argv)
- # Get dictionary of all options
- args = vars(options)
- return Run(**args)
+ # Get dictionary of all options
+ args = vars(options)
+ return Run(**args)
-if __name__ == '__main__':
- sys.exit(Main(sys.argv[1:]))
+if __name__ == "__main__":
+ sys.exit(Main(sys.argv[1:]))