aboutsummaryrefslogtreecommitdiff
path: root/afdo_tools/bisection/afdo_prof_analysis.py
diff options
context:
space:
mode:
Diffstat (limited to 'afdo_tools/bisection/afdo_prof_analysis.py')
-rwxr-xr-xafdo_tools/bisection/afdo_prof_analysis.py784
1 files changed, 415 insertions, 369 deletions
diff --git a/afdo_tools/bisection/afdo_prof_analysis.py b/afdo_tools/bisection/afdo_prof_analysis.py
index ce8afd64..c9ca9214 100755
--- a/afdo_tools/bisection/afdo_prof_analysis.py
+++ b/afdo_tools/bisection/afdo_prof_analysis.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-# Copyright 2019 The Chromium OS Authors. All rights reserved.
+# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
@@ -20,7 +20,6 @@ exit code. The codes known to this script are:
- >127: quit immediately
"""
-from __future__ import division, print_function
import argparse
import json
@@ -35,11 +34,12 @@ from tempfile import mkstemp
class StatusEnum(IntEnum):
- """Enum of valid statuses returned by profile decider."""
- GOOD_STATUS = 0
- BAD_STATUS = 1
- SKIP_STATUS = 125
- PROBLEM_STATUS = 127
+ """Enum of valid statuses returned by profile decider."""
+
+ GOOD_STATUS = 0
+ BAD_STATUS = 1
+ SKIP_STATUS = 125
+ PROBLEM_STATUS = 127
statuses = StatusEnum.__members__.values()
@@ -48,396 +48,442 @@ _NUM_RUNS_RANGE_SEARCH = 20 # how many times range search should run its algo
def json_to_text(json_prof):
- text_profile = []
- for func in json_prof:
- text_profile.append(func)
- text_profile.append(json_prof[func])
- return ''.join(text_profile)
+ text_profile = []
+ for func in json_prof:
+ text_profile.append(func)
+ text_profile.append(json_prof[func])
+ return "".join(text_profile)
def text_to_json(f):
- """Performs basic parsing of an AFDO text-based profile.
-
- This parsing expects an input file object with contents of the form generated
- by bin/llvm-profdata (within an LLVM build).
- """
- results = {}
- curr_func = None
- curr_data = []
- for line in f:
- if not line.startswith(' '):
- if curr_func:
- results[curr_func] = ''.join(curr_data)
- curr_data = []
- curr_func, rest = line.split(':', 1)
- curr_func = curr_func.strip()
- curr_data.append(':' + rest)
- else:
- curr_data.append(line)
-
- if curr_func:
- results[curr_func] = ''.join(curr_data)
- return results
+ """Performs basic parsing of an AFDO text-based profile.
+
+ This parsing expects an input file object with contents of the form generated
+ by bin/llvm-profdata (within an LLVM build).
+ """
+ results = {}
+ curr_func = None
+ curr_data = []
+ for line in f:
+ if not line.startswith(" "):
+ if curr_func:
+ results[curr_func] = "".join(curr_data)
+ curr_data = []
+ curr_func, rest = line.split(":", 1)
+ curr_func = curr_func.strip()
+ curr_data.append(":" + rest)
+ else:
+ curr_data.append(line)
+
+ if curr_func:
+ results[curr_func] = "".join(curr_data)
+ return results
def prof_to_tmp(prof):
- """Creates (and returns) temp filename for given JSON-based AFDO profile."""
- fd, temp_path = mkstemp()
- text_profile = json_to_text(prof)
- with open(temp_path, 'w') as f:
- f.write(text_profile)
- os.close(fd)
- return temp_path
+ """Creates (and returns) temp filename for given JSON-based AFDO profile."""
+ fd, temp_path = mkstemp()
+ text_profile = json_to_text(prof)
+ with open(temp_path, "w") as f:
+ f.write(text_profile)
+ os.close(fd)
+ return temp_path
class DeciderState(object):
- """Class for the external decider."""
-
- def __init__(self, state_file, external_decider, seed):
- self.accumulated_results = [] # over this run of the script
- self.external_decider = external_decider
- self.saved_results = [] # imported from a previous run of this script
- self.state_file = state_file
- self.seed = seed if seed is not None else time.time()
-
- def load_state(self):
- if not os.path.exists(self.state_file):
- logging.info('State file %s is empty, starting from beginning',
- self.state_file)
- return
-
- with open(self.state_file, encoding='utf-8') as f:
- try:
- data = json.load(f)
- except:
- raise ValueError('Provided state file %s to resume from does not'
- ' contain a valid JSON.' % self.state_file)
-
- if 'seed' not in data or 'accumulated_results' not in data:
- raise ValueError('Provided state file %s to resume from does not contain'
- ' the correct information' % self.state_file)
-
- self.seed = data['seed']
- self.saved_results = data['accumulated_results']
- logging.info('Restored state from %s...', self.state_file)
-
- def save_state(self):
- state = {'seed': self.seed, 'accumulated_results': self.accumulated_results}
- tmp_file = self.state_file + '.new'
- with open(tmp_file, 'w', encoding='utf-8') as f:
- json.dump(state, f, indent=2)
- os.rename(tmp_file, self.state_file)
- logging.info('Logged state to %s...', self.state_file)
-
- def run(self, prof, save_run=True):
- """Run the external deciding script on the given profile."""
- if self.saved_results and save_run:
- result = self.saved_results.pop(0)
- self.accumulated_results.append(result)
- self.save_state()
- return StatusEnum(result)
-
- filename = prof_to_tmp(prof)
-
- try:
- return_code = subprocess.call([self.external_decider, filename])
- finally:
- os.remove(filename)
-
- if return_code in statuses:
- status = StatusEnum(return_code)
- if status == StatusEnum.PROBLEM_STATUS:
- prof_file = prof_to_tmp(prof)
- raise RuntimeError('Provided decider script returned PROBLEM_STATUS '
- 'when run on profile stored at %s. AFDO Profile '
- 'analysis aborting' % prof_file)
- if save_run:
- self.accumulated_results.append(status.value)
- logging.info('Run %d of external script %s returned %s',
- len(self.accumulated_results), self.external_decider,
- status.name)
- self.save_state()
- return status
- raise ValueError(
- 'Provided external script had unexpected return code %d' % return_code)
+ """Class for the external decider."""
+
+ def __init__(self, state_file, external_decider, seed):
+ self.accumulated_results = [] # over this run of the script
+ self.external_decider = external_decider
+ self.saved_results = [] # imported from a previous run of this script
+ self.state_file = state_file
+ self.seed = seed if seed is not None else time.time()
+
+ def load_state(self):
+ if not os.path.exists(self.state_file):
+ logging.info(
+ "State file %s is empty, starting from beginning",
+ self.state_file,
+ )
+ return
+
+ with open(self.state_file, encoding="utf-8") as f:
+ try:
+ data = json.load(f)
+ except:
+ raise ValueError(
+ "Provided state file %s to resume from does not"
+ " contain a valid JSON." % self.state_file
+ )
+
+ if "seed" not in data or "accumulated_results" not in data:
+ raise ValueError(
+ "Provided state file %s to resume from does not contain"
+ " the correct information" % self.state_file
+ )
+
+ self.seed = data["seed"]
+ self.saved_results = data["accumulated_results"]
+ logging.info("Restored state from %s...", self.state_file)
+
+ def save_state(self):
+ state = {
+ "seed": self.seed,
+ "accumulated_results": self.accumulated_results,
+ }
+ tmp_file = self.state_file + ".new"
+ with open(tmp_file, "w", encoding="utf-8") as f:
+ json.dump(state, f, indent=2)
+ os.rename(tmp_file, self.state_file)
+ logging.info("Logged state to %s...", self.state_file)
+
+ def run(self, prof, save_run=True):
+ """Run the external deciding script on the given profile."""
+ if self.saved_results and save_run:
+ result = self.saved_results.pop(0)
+ self.accumulated_results.append(result)
+ self.save_state()
+ return StatusEnum(result)
+
+ filename = prof_to_tmp(prof)
+
+ try:
+ return_code = subprocess.call([self.external_decider, filename])
+ finally:
+ os.remove(filename)
+
+ if return_code in statuses:
+ status = StatusEnum(return_code)
+ if status == StatusEnum.PROBLEM_STATUS:
+ prof_file = prof_to_tmp(prof)
+ raise RuntimeError(
+ "Provided decider script returned PROBLEM_STATUS "
+ "when run on profile stored at %s. AFDO Profile "
+ "analysis aborting" % prof_file
+ )
+ if save_run:
+ self.accumulated_results.append(status.value)
+ logging.info(
+ "Run %d of external script %s returned %s",
+ len(self.accumulated_results),
+ self.external_decider,
+ status.name,
+ )
+ self.save_state()
+ return status
+ raise ValueError(
+ "Provided external script had unexpected return code %d"
+ % return_code
+ )
def bisect_profiles(decider, good, bad, common_funcs, lo, hi):
- """Recursive function which bisects good and bad profiles.
-
- Args:
- decider: function which, given a JSON-based AFDO profile, returns an
- element of 'statuses' based on the status of the profile
- good: JSON-based good AFDO profile
- bad: JSON-based bad AFDO profile
- common_funcs: the list of functions which have top-level profiles in both
- 'good' and 'bad'
- lo: lower bound of range being bisected on
- hi: upper bound of range being bisected on
-
- Returns a dictionary with two keys: 'individuals' and 'ranges'.
- 'individuals': a list of individual functions found to make the profile BAD
- 'ranges': a list of lists of function names. Each list of functions is a list
- such that including all of those from the bad profile makes the good
- profile BAD. It may not be the smallest problematic combination, but
- definitely contains a problematic combination of profiles.
- """
-
- results = {'individuals': [], 'ranges': []}
- if hi - lo <= 1:
- logging.info('Found %s as a problematic function profile', common_funcs[lo])
- results['individuals'].append(common_funcs[lo])
- return results
+ """Recursive function which bisects good and bad profiles.
+
+ Args:
+ decider: function which, given a JSON-based AFDO profile, returns an
+ element of 'statuses' based on the status of the profile
+ good: JSON-based good AFDO profile
+ bad: JSON-based bad AFDO profile
+ common_funcs: the list of functions which have top-level profiles in both
+ 'good' and 'bad'
+ lo: lower bound of range being bisected on
+ hi: upper bound of range being bisected on
+
+ Returns a dictionary with two keys: 'individuals' and 'ranges'.
+ 'individuals': a list of individual functions found to make the profile BAD
+ 'ranges': a list of lists of function names. Each list of functions is a list
+ such that including all of those from the bad profile makes the good
+ profile BAD. It may not be the smallest problematic combination, but
+ definitely contains a problematic combination of profiles.
+ """
+
+ results = {"individuals": [], "ranges": []}
+ if hi - lo <= 1:
+ logging.info(
+ "Found %s as a problematic function profile", common_funcs[lo]
+ )
+ results["individuals"].append(common_funcs[lo])
+ return results
+
+ mid = (lo + hi) // 2
+ lo_mid_prof = good.copy() # covers bad from lo:mid
+ mid_hi_prof = good.copy() # covers bad from mid:hi
+ for func in common_funcs[lo:mid]:
+ lo_mid_prof[func] = bad[func]
+ for func in common_funcs[mid:hi]:
+ mid_hi_prof[func] = bad[func]
+
+ lo_mid_verdict = decider.run(lo_mid_prof)
+ mid_hi_verdict = decider.run(mid_hi_prof)
+
+ if lo_mid_verdict == StatusEnum.BAD_STATUS:
+ result = bisect_profiles(decider, good, bad, common_funcs, lo, mid)
+ results["individuals"].extend(result["individuals"])
+ results["ranges"].extend(result["ranges"])
+ if mid_hi_verdict == StatusEnum.BAD_STATUS:
+ result = bisect_profiles(decider, good, bad, common_funcs, mid, hi)
+ results["individuals"].extend(result["individuals"])
+ results["ranges"].extend(result["ranges"])
+
+ # neither half is bad -> the issue is caused by several things occuring
+ # in conjunction, and this combination crosses 'mid'
+ if lo_mid_verdict == mid_hi_verdict == StatusEnum.GOOD_STATUS:
+ problem_range = range_search(decider, good, bad, common_funcs, lo, hi)
+ if problem_range:
+ logging.info(
+ "Found %s as a problematic combination of profiles",
+ str(problem_range),
+ )
+ results["ranges"].append(problem_range)
- mid = (lo + hi) // 2
- lo_mid_prof = good.copy() # covers bad from lo:mid
- mid_hi_prof = good.copy() # covers bad from mid:hi
- for func in common_funcs[lo:mid]:
- lo_mid_prof[func] = bad[func]
- for func in common_funcs[mid:hi]:
- mid_hi_prof[func] = bad[func]
-
- lo_mid_verdict = decider.run(lo_mid_prof)
- mid_hi_verdict = decider.run(mid_hi_prof)
-
- if lo_mid_verdict == StatusEnum.BAD_STATUS:
- result = bisect_profiles(decider, good, bad, common_funcs, lo, mid)
- results['individuals'].extend(result['individuals'])
- results['ranges'].extend(result['ranges'])
- if mid_hi_verdict == StatusEnum.BAD_STATUS:
- result = bisect_profiles(decider, good, bad, common_funcs, mid, hi)
- results['individuals'].extend(result['individuals'])
- results['ranges'].extend(result['ranges'])
-
- # neither half is bad -> the issue is caused by several things occuring
- # in conjunction, and this combination crosses 'mid'
- if lo_mid_verdict == mid_hi_verdict == StatusEnum.GOOD_STATUS:
- problem_range = range_search(decider, good, bad, common_funcs, lo, hi)
- if problem_range:
- logging.info('Found %s as a problematic combination of profiles',
- str(problem_range))
- results['ranges'].append(problem_range)
-
- return results
+ return results
def bisect_profiles_wrapper(decider, good, bad, perform_check=True):
- """Wrapper for recursive profile bisection."""
-
- # Validate good and bad profiles are such, otherwise bisection reports noise
- # Note that while decider is a random mock, these assertions may fail.
- if perform_check:
- if decider.run(good, save_run=False) != StatusEnum.GOOD_STATUS:
- raise ValueError('Supplied good profile is not actually GOOD')
- if decider.run(bad, save_run=False) != StatusEnum.BAD_STATUS:
- raise ValueError('Supplied bad profile is not actually BAD')
-
- common_funcs = sorted(func for func in good if func in bad)
- if not common_funcs:
- return {'ranges': [], 'individuals': []}
-
- # shuffle because the results of our analysis can be quite order-dependent
- # but this list has no inherent ordering. By shuffling each time, the chances
- # of finding new, potentially interesting results are increased each time
- # the program is run
- random.shuffle(common_funcs)
- results = bisect_profiles(decider, good, bad, common_funcs, 0,
- len(common_funcs))
- results['ranges'].sort()
- results['individuals'].sort()
- return results
+ """Wrapper for recursive profile bisection."""
+
+ # Validate good and bad profiles are such, otherwise bisection reports noise
+ # Note that while decider is a random mock, these assertions may fail.
+ if perform_check:
+ if decider.run(good, save_run=False) != StatusEnum.GOOD_STATUS:
+ raise ValueError("Supplied good profile is not actually GOOD")
+ if decider.run(bad, save_run=False) != StatusEnum.BAD_STATUS:
+ raise ValueError("Supplied bad profile is not actually BAD")
+
+ common_funcs = sorted(func for func in good if func in bad)
+ if not common_funcs:
+ return {"ranges": [], "individuals": []}
+
+ # shuffle because the results of our analysis can be quite order-dependent
+ # but this list has no inherent ordering. By shuffling each time, the chances
+ # of finding new, potentially interesting results are increased each time
+ # the program is run
+ random.shuffle(common_funcs)
+ results = bisect_profiles(
+ decider, good, bad, common_funcs, 0, len(common_funcs)
+ )
+ results["ranges"].sort()
+ results["individuals"].sort()
+ return results
def range_search(decider, good, bad, common_funcs, lo, hi):
- """Searches for problematic range crossing mid border.
-
- The main inner algorithm is the following, which looks for the smallest
- possible ranges with problematic combinations. It starts the upper bound at
- the midpoint, and increments in halves until it gets a BAD profile.
- Then, it increments the lower bound (in halves) until the resultant profile
- is GOOD, and then we have a range that causes 'BAD'ness.
-
- It does this _NUM_RUNS_RANGE_SEARCH times, and shuffles the functions being
- looked at uniquely each time to try and get the smallest possible range
- of functions in a reasonable timeframe.
- """
-
- average = lambda x, y: int(round((x + y) // 2.0))
-
- def find_upper_border(good_copy, funcs, lo, hi, last_bad_val=None):
- """Finds the upper border of problematic range."""
- mid = average(lo, hi)
- if mid in (lo, hi):
- return last_bad_val or hi
-
- for func in funcs[lo:mid]:
- good_copy[func] = bad[func]
- verdict = decider.run(good_copy)
-
- # reset for next iteration
- for func in funcs:
- good_copy[func] = good[func]
-
- if verdict == StatusEnum.BAD_STATUS:
- return find_upper_border(good_copy, funcs, lo, mid, mid)
- return find_upper_border(good_copy, funcs, mid, hi, last_bad_val)
-
- def find_lower_border(good_copy, funcs, lo, hi, last_bad_val=None):
- """Finds the lower border of problematic range."""
- mid = average(lo, hi)
- if mid in (lo, hi):
- return last_bad_val or lo
-
- for func in funcs[lo:mid]:
- good_copy[func] = good[func]
- verdict = decider.run(good_copy)
-
- # reset for next iteration
- for func in funcs:
- good_copy[func] = bad[func]
-
- if verdict == StatusEnum.BAD_STATUS:
- return find_lower_border(good_copy, funcs, mid, hi, lo)
- return find_lower_border(good_copy, funcs, lo, mid, last_bad_val)
-
- lo_mid_funcs = []
- mid_hi_funcs = []
- min_range_funcs = []
- for _ in range(_NUM_RUNS_RANGE_SEARCH):
-
- if min_range_funcs: # only examine range we've already narrowed to
- random.shuffle(lo_mid_funcs)
- random.shuffle(mid_hi_funcs)
- else: # consider lo-mid and mid-hi separately bc must cross border
- mid = (lo + hi) // 2
- lo_mid_funcs = common_funcs[lo:mid]
- mid_hi_funcs = common_funcs[mid:hi]
-
- funcs = lo_mid_funcs + mid_hi_funcs
- hi = len(funcs)
- mid = len(lo_mid_funcs)
- lo = 0
-
- # because we need the problematic pair to pop up before we can narrow it
- prof = good.copy()
- for func in lo_mid_funcs:
- prof[func] = bad[func]
-
- upper_border = find_upper_border(prof, funcs, mid, hi)
- for func in lo_mid_funcs + funcs[mid:upper_border]:
- prof[func] = bad[func]
-
- lower_border = find_lower_border(prof, funcs, lo, mid)
- curr_range_funcs = funcs[lower_border:upper_border]
-
- if not min_range_funcs or len(curr_range_funcs) < len(min_range_funcs):
- min_range_funcs = curr_range_funcs
- lo_mid_funcs = lo_mid_funcs[lo_mid_funcs.index(min_range_funcs[0]):]
- mid_hi_funcs = mid_hi_funcs[:mid_hi_funcs.index(min_range_funcs[-1]) + 1]
- if len(min_range_funcs) == 2:
- min_range_funcs.sort()
- return min_range_funcs # can't get any smaller
-
- min_range_funcs.sort()
- return min_range_funcs
+ """Searches for problematic range crossing mid border.
+
+ The main inner algorithm is the following, which looks for the smallest
+ possible ranges with problematic combinations. It starts the upper bound at
+ the midpoint, and increments in halves until it gets a BAD profile.
+ Then, it increments the lower bound (in halves) until the resultant profile
+ is GOOD, and then we have a range that causes 'BAD'ness.
+
+ It does this _NUM_RUNS_RANGE_SEARCH times, and shuffles the functions being
+ looked at uniquely each time to try and get the smallest possible range
+ of functions in a reasonable timeframe.
+ """
+
+ average = lambda x, y: int(round((x + y) // 2.0))
+
+ def find_upper_border(good_copy, funcs, lo, hi, last_bad_val=None):
+ """Finds the upper border of problematic range."""
+ mid = average(lo, hi)
+ if mid in (lo, hi):
+ return last_bad_val or hi
+
+ for func in funcs[lo:mid]:
+ good_copy[func] = bad[func]
+ verdict = decider.run(good_copy)
+
+ # reset for next iteration
+ for func in funcs:
+ good_copy[func] = good[func]
+
+ if verdict == StatusEnum.BAD_STATUS:
+ return find_upper_border(good_copy, funcs, lo, mid, mid)
+ return find_upper_border(good_copy, funcs, mid, hi, last_bad_val)
+
+ def find_lower_border(good_copy, funcs, lo, hi, last_bad_val=None):
+ """Finds the lower border of problematic range."""
+ mid = average(lo, hi)
+ if mid in (lo, hi):
+ return last_bad_val or lo
+
+ for func in funcs[lo:mid]:
+ good_copy[func] = good[func]
+ verdict = decider.run(good_copy)
+
+ # reset for next iteration
+ for func in funcs:
+ good_copy[func] = bad[func]
+
+ if verdict == StatusEnum.BAD_STATUS:
+ return find_lower_border(good_copy, funcs, mid, hi, lo)
+ return find_lower_border(good_copy, funcs, lo, mid, last_bad_val)
+
+ lo_mid_funcs = []
+ mid_hi_funcs = []
+ min_range_funcs = []
+ for _ in range(_NUM_RUNS_RANGE_SEARCH):
+
+ if min_range_funcs: # only examine range we've already narrowed to
+ random.shuffle(lo_mid_funcs)
+ random.shuffle(mid_hi_funcs)
+ else: # consider lo-mid and mid-hi separately bc must cross border
+ mid = (lo + hi) // 2
+ lo_mid_funcs = common_funcs[lo:mid]
+ mid_hi_funcs = common_funcs[mid:hi]
+
+ funcs = lo_mid_funcs + mid_hi_funcs
+ hi = len(funcs)
+ mid = len(lo_mid_funcs)
+ lo = 0
+
+ # because we need the problematic pair to pop up before we can narrow it
+ prof = good.copy()
+ for func in lo_mid_funcs:
+ prof[func] = bad[func]
+
+ upper_border = find_upper_border(prof, funcs, mid, hi)
+ for func in lo_mid_funcs + funcs[mid:upper_border]:
+ prof[func] = bad[func]
+
+ lower_border = find_lower_border(prof, funcs, lo, mid)
+ curr_range_funcs = funcs[lower_border:upper_border]
+
+ if not min_range_funcs or len(curr_range_funcs) < len(min_range_funcs):
+ min_range_funcs = curr_range_funcs
+ lo_mid_funcs = lo_mid_funcs[
+ lo_mid_funcs.index(min_range_funcs[0]) :
+ ]
+ mid_hi_funcs = mid_hi_funcs[
+ : mid_hi_funcs.index(min_range_funcs[-1]) + 1
+ ]
+ if len(min_range_funcs) == 2:
+ min_range_funcs.sort()
+ return min_range_funcs # can't get any smaller
+
+ min_range_funcs.sort()
+ return min_range_funcs
def check_good_not_bad(decider, good, bad):
- """Check if bad prof becomes GOOD by adding funcs it lacks from good prof"""
- bad_copy = bad.copy()
- for func in good:
- if func not in bad:
- bad_copy[func] = good[func]
- return decider.run(bad_copy) == StatusEnum.GOOD_STATUS
+ """Check if bad prof becomes GOOD by adding funcs it lacks from good prof"""
+ bad_copy = bad.copy()
+ for func in good:
+ if func not in bad:
+ bad_copy[func] = good[func]
+ return decider.run(bad_copy) == StatusEnum.GOOD_STATUS
def check_bad_not_good(decider, good, bad):
- """Check if good prof BAD after adding funcs bad prof has that good doesnt"""
- good_copy = good.copy()
- for func in bad:
- if func not in good:
- good_copy[func] = bad[func]
- return decider.run(good_copy) == StatusEnum.BAD_STATUS
+ """Check if good prof BAD after adding funcs bad prof has that good doesnt"""
+ good_copy = good.copy()
+ for func in bad:
+ if func not in good:
+ good_copy[func] = bad[func]
+ return decider.run(good_copy) == StatusEnum.BAD_STATUS
def parse_args():
- parser = argparse.ArgumentParser(
- description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
- parser.add_argument(
- '--good_prof',
- required=True,
- help='Text-based "Good" profile for analysis')
- parser.add_argument(
- '--bad_prof', required=True, help='Text-based "Bad" profile for analysis')
- parser.add_argument(
- '--external_decider',
- required=True,
- help='External script that, given an AFDO profile, returns '
- 'GOOD/BAD/SKIP')
- parser.add_argument(
- '--analysis_output_file',
- required=True,
- help='File to output JSON results to')
- parser.add_argument(
- '--state_file',
- default='%s/afdo_analysis_state.json' % os.getcwd(),
- help='File path containing state to load from initially, and will be '
- 'overwritten with new state on each iteration')
- parser.add_argument(
- '--no_resume',
- action='store_true',
- help='If enabled, no initial state will be loaded and the program will '
- 'run from the beginning')
- parser.add_argument(
- '--remove_state_on_completion',
- action='store_true',
- help='If enabled, state file will be removed once profile analysis is '
- 'completed')
- parser.add_argument(
- '--seed', type=float, help='Float specifying seed for randomness')
- return parser.parse_args()
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument(
+ "--good_prof",
+ required=True,
+ help='Text-based "Good" profile for analysis',
+ )
+ parser.add_argument(
+ "--bad_prof",
+ required=True,
+ help='Text-based "Bad" profile for analysis',
+ )
+ parser.add_argument(
+ "--external_decider",
+ required=True,
+ help="External script that, given an AFDO profile, returns "
+ "GOOD/BAD/SKIP",
+ )
+ parser.add_argument(
+ "--analysis_output_file",
+ required=True,
+ help="File to output JSON results to",
+ )
+ parser.add_argument(
+ "--state_file",
+ default="%s/afdo_analysis_state.json" % os.getcwd(),
+ help="File path containing state to load from initially, and will be "
+ "overwritten with new state on each iteration",
+ )
+ parser.add_argument(
+ "--no_resume",
+ action="store_true",
+ help="If enabled, no initial state will be loaded and the program will "
+ "run from the beginning",
+ )
+ parser.add_argument(
+ "--remove_state_on_completion",
+ action="store_true",
+ help="If enabled, state file will be removed once profile analysis is "
+ "completed",
+ )
+ parser.add_argument(
+ "--seed", type=float, help="Float specifying seed for randomness"
+ )
+ return parser.parse_args()
def main(flags):
- logging.getLogger().setLevel(logging.INFO)
- if not flags.no_resume and flags.seed: # conflicting seeds
- raise RuntimeError('Ambiguous seed value; do not resume from existing '
- 'state and also specify seed by command line flag')
-
- decider = DeciderState(
- flags.state_file, flags.external_decider, seed=flags.seed)
- if not flags.no_resume:
- decider.load_state()
- random.seed(decider.seed)
-
- with open(flags.good_prof) as good_f:
- good_items = text_to_json(good_f)
- with open(flags.bad_prof) as bad_f:
- bad_items = text_to_json(bad_f)
-
- bisect_results = bisect_profiles_wrapper(decider, good_items, bad_items)
- gnb_result = check_good_not_bad(decider, good_items, bad_items)
- bng_result = check_bad_not_good(decider, good_items, bad_items)
-
- results = {
- 'seed': decider.seed,
- 'bisect_results': bisect_results,
- 'good_only_functions': gnb_result,
- 'bad_only_functions': bng_result
- }
- with open(flags.analysis_output_file, 'w', encoding='utf-8') as f:
- json.dump(results, f, indent=2)
- if flags.remove_state_on_completion:
- os.remove(flags.state_file)
- logging.info('Removed state file %s following completion of script...',
- flags.state_file)
- else:
- completed_state_file = '%s.completed.%s' % (flags.state_file,
- str(date.today()))
- os.rename(flags.state_file, completed_state_file)
- logging.info('Stored completed state file as %s...', completed_state_file)
- return results
-
-
-if __name__ == '__main__':
- main(parse_args())
+ logging.getLogger().setLevel(logging.INFO)
+ if not flags.no_resume and flags.seed: # conflicting seeds
+ raise RuntimeError(
+ "Ambiguous seed value; do not resume from existing "
+ "state and also specify seed by command line flag"
+ )
+
+ decider = DeciderState(
+ flags.state_file, flags.external_decider, seed=flags.seed
+ )
+ if not flags.no_resume:
+ decider.load_state()
+ random.seed(decider.seed)
+
+ with open(flags.good_prof) as good_f:
+ good_items = text_to_json(good_f)
+ with open(flags.bad_prof) as bad_f:
+ bad_items = text_to_json(bad_f)
+
+ bisect_results = bisect_profiles_wrapper(decider, good_items, bad_items)
+ gnb_result = check_good_not_bad(decider, good_items, bad_items)
+ bng_result = check_bad_not_good(decider, good_items, bad_items)
+
+ results = {
+ "seed": decider.seed,
+ "bisect_results": bisect_results,
+ "good_only_functions": gnb_result,
+ "bad_only_functions": bng_result,
+ }
+ with open(flags.analysis_output_file, "w", encoding="utf-8") as f:
+ json.dump(results, f, indent=2)
+ if flags.remove_state_on_completion:
+ os.remove(flags.state_file)
+ logging.info(
+ "Removed state file %s following completion of script...",
+ flags.state_file,
+ )
+ else:
+ completed_state_file = "%s.completed.%s" % (
+ flags.state_file,
+ str(date.today()),
+ )
+ os.rename(flags.state_file, completed_state_file)
+ logging.info(
+ "Stored completed state file as %s...", completed_state_file
+ )
+ return results
+
+
+if __name__ == "__main__":
+ main(parse_args())