aboutsummaryrefslogtreecommitdiff
path: root/afdo_tools
diff options
context:
space:
mode:
Diffstat (limited to 'afdo_tools')
-rwxr-xr-xafdo_tools/bisection/afdo_prof_analysis.py784
-rwxr-xr-xafdo_tools/bisection/afdo_prof_analysis_e2e_test.py505
-rwxr-xr-xafdo_tools/bisection/afdo_prof_analysis_test.py290
-rwxr-xr-xafdo_tools/bisection/state_assumption_external.sh2
-rwxr-xr-xafdo_tools/bisection/state_assumption_interrupt.sh2
-rwxr-xr-xafdo_tools/generate_afdo_from_tryjob.py266
-rwxr-xr-xafdo_tools/run_afdo_tryjob.py231
-rwxr-xr-xafdo_tools/update_kernel_afdo308
8 files changed, 1296 insertions, 1092 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())
diff --git a/afdo_tools/bisection/afdo_prof_analysis_e2e_test.py b/afdo_tools/bisection/afdo_prof_analysis_e2e_test.py
index b293b8aa..8a0dae38 100755
--- a/afdo_tools/bisection/afdo_prof_analysis_e2e_test.py
+++ b/afdo_tools/bisection/afdo_prof_analysis_e2e_test.py
@@ -1,12 +1,11 @@
#!/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.
"""End-to-end test for afdo_prof_analysis."""
-from __future__ import absolute_import, division, print_function
import json
import os
@@ -19,263 +18,273 @@ from afdo_tools.bisection import afdo_prof_analysis as analysis
class ObjectWithFields(object):
- """Turns kwargs given to the constructor into fields on an object.
+ """Turns kwargs given to the constructor into fields on an object.
- Examples:
- x = ObjectWithFields(a=1, b=2)
- assert x.a == 1
- assert x.b == 2
- """
+ Examples:
+ x = ObjectWithFields(a=1, b=2)
+ assert x.a == 1
+ assert x.b == 2
+ """
- def __init__(self, **kwargs):
- for key, val in kwargs.items():
- setattr(self, key, val)
+ def __init__(self, **kwargs):
+ for key, val in kwargs.items():
+ setattr(self, key, val)
class AfdoProfAnalysisE2ETest(unittest.TestCase):
- """Class for end-to-end testing of AFDO Profile Analysis"""
-
- # nothing significant about the values, just easier to remember even vs odd
- good_prof = {
- 'func_a': ':1\n 1: 3\n 3: 5\n 5: 7\n',
- 'func_b': ':3\n 3: 5\n 5: 7\n 7: 9\n',
- 'func_c': ':5\n 5: 7\n 7: 9\n 9: 11\n',
- 'func_d': ':7\n 7: 9\n 9: 11\n 11: 13\n',
- 'good_func_a': ':11\n',
- 'good_func_b': ':13\n'
- }
-
- bad_prof = {
- 'func_a': ':2\n 2: 4\n 4: 6\n 6: 8\n',
- 'func_b': ':4\n 4: 6\n 6: 8\n 8: 10\n',
- 'func_c': ':6\n 6: 8\n 8: 10\n 10: 12\n',
- 'func_d': ':8\n 8: 10\n 10: 12\n 12: 14\n',
- 'bad_func_a': ':12\n',
- 'bad_func_b': ':14\n'
- }
-
- expected = {
- 'good_only_functions': False,
- 'bad_only_functions': True,
- 'bisect_results': {
- 'ranges': [],
- 'individuals': ['func_a']
- }
- }
-
- def test_afdo_prof_analysis(self):
- # Individual issues take precedence by nature of our algos
- # so first, that should be caught
- good = self.good_prof.copy()
- bad = self.bad_prof.copy()
- self.run_check(good, bad, self.expected)
-
- # Now remove individuals and exclusively BAD, and check that range is caught
- bad['func_a'] = good['func_a']
- bad.pop('bad_func_a')
- bad.pop('bad_func_b')
-
- expected_cp = self.expected.copy()
- expected_cp['bad_only_functions'] = False
- expected_cp['bisect_results'] = {
- 'individuals': [],
- 'ranges': [['func_b', 'func_c', 'func_d']]
+ """Class for end-to-end testing of AFDO Profile Analysis"""
+
+ # nothing significant about the values, just easier to remember even vs odd
+ good_prof = {
+ "func_a": ":1\n 1: 3\n 3: 5\n 5: 7\n",
+ "func_b": ":3\n 3: 5\n 5: 7\n 7: 9\n",
+ "func_c": ":5\n 5: 7\n 7: 9\n 9: 11\n",
+ "func_d": ":7\n 7: 9\n 9: 11\n 11: 13\n",
+ "good_func_a": ":11\n",
+ "good_func_b": ":13\n",
+ }
+
+ bad_prof = {
+ "func_a": ":2\n 2: 4\n 4: 6\n 6: 8\n",
+ "func_b": ":4\n 4: 6\n 6: 8\n 8: 10\n",
+ "func_c": ":6\n 6: 8\n 8: 10\n 10: 12\n",
+ "func_d": ":8\n 8: 10\n 10: 12\n 12: 14\n",
+ "bad_func_a": ":12\n",
+ "bad_func_b": ":14\n",
}
- self.run_check(good, bad, expected_cp)
-
- def test_afdo_prof_state(self):
- """Verifies that saved state is correct replication."""
- temp_dir = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, temp_dir, ignore_errors=True)
-
- good = self.good_prof.copy()
- bad = self.bad_prof.copy()
- # add more functions to data
- for x in range(400):
- good['func_%d' % x] = ''
- bad['func_%d' % x] = ''
-
- fd_first, first_result = tempfile.mkstemp(dir=temp_dir)
- os.close(fd_first)
- fd_state, state_file = tempfile.mkstemp(dir=temp_dir)
- os.close(fd_state)
- self.run_check(
- self.good_prof,
- self.bad_prof,
- self.expected,
- state_file=state_file,
- out_file=first_result)
-
- fd_second, second_result = tempfile.mkstemp(dir=temp_dir)
- os.close(fd_second)
- completed_state_file = '%s.completed.%s' % (state_file, str(date.today()))
- self.run_check(
- self.good_prof,
- self.bad_prof,
- self.expected,
- state_file=completed_state_file,
- no_resume=False,
- out_file=second_result)
-
- with open(first_result) as f:
- initial_run = json.load(f)
- with open(second_result) as f:
- loaded_run = json.load(f)
- self.assertEqual(initial_run, loaded_run)
-
- def test_exit_on_problem_status(self):
- temp_dir = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, temp_dir, ignore_errors=True)
-
- fd_state, state_file = tempfile.mkstemp(dir=temp_dir)
- os.close(fd_state)
- with self.assertRaises(RuntimeError):
- self.run_check(
- self.good_prof,
- self.bad_prof,
- self.expected,
- state_file=state_file,
- extern_decider='problemstatus_external.sh')
-
- def test_state_assumption(self):
-
- def compare_runs(tmp_dir, first_ctr, second_ctr):
- """Compares given prof versions between first and second run in test."""
- first_prof = '%s/.first_run_%d' % (tmp_dir, first_ctr)
- second_prof = '%s/.second_run_%d' % (tmp_dir, second_ctr)
- with open(first_prof) as f:
- first_prof_text = f.read()
- with open(second_prof) as f:
- second_prof_text = f.read()
- self.assertEqual(first_prof_text, second_prof_text)
-
- good_prof = {'func_a': ':1\n3: 3\n5: 7\n'}
- bad_prof = {'func_a': ':2\n4: 4\n6: 8\n'}
- # add some noise to the profiles; 15 is an arbitrary choice
- for x in range(15):
- func = 'func_%d' % x
- good_prof[func] = ':%d\n' % (x)
- bad_prof[func] = ':%d\n' % (x + 1)
expected = {
- 'bisect_results': {
- 'ranges': [],
- 'individuals': ['func_a']
- },
- 'good_only_functions': False,
- 'bad_only_functions': False
+ "good_only_functions": False,
+ "bad_only_functions": True,
+ "bisect_results": {"ranges": [], "individuals": ["func_a"]},
}
- # using a static temp dir rather than a dynamic one because these files are
- # shared between the bash scripts and this Python test, and the arguments
- # to the bash scripts are fixed by afdo_prof_analysis.py so it would be
- # difficult to communicate dynamically generated directory to bash scripts
- scripts_tmp_dir = '%s/afdo_test_tmp' % os.getcwd()
- os.mkdir(scripts_tmp_dir)
- self.addCleanup(shutil.rmtree, scripts_tmp_dir, ignore_errors=True)
-
- # files used in the bash scripts used as external deciders below
- # - count_file tracks the current number of calls to the script in total
- # - local_count_file tracks the number of calls to the script without
- # interruption
- count_file = '%s/.count' % scripts_tmp_dir
- local_count_file = '%s/.local_count' % scripts_tmp_dir
-
- # runs through whole thing at once
- initial_seed = self.run_check(
- good_prof,
- bad_prof,
- expected,
- extern_decider='state_assumption_external.sh')
- with open(count_file) as f:
- num_calls = int(f.read())
- os.remove(count_file) # reset counts for second run
- finished_state_file = 'afdo_analysis_state.json.completed.%s' % str(
- date.today())
- self.addCleanup(os.remove, finished_state_file)
-
- # runs the same analysis but interrupted each iteration
- for i in range(2 * num_calls + 1):
- no_resume_run = (i == 0)
- seed = initial_seed if no_resume_run else None
- try:
+ def test_afdo_prof_analysis(self):
+ # Individual issues take precedence by nature of our algos
+ # so first, that should be caught
+ good = self.good_prof.copy()
+ bad = self.bad_prof.copy()
+ self.run_check(good, bad, self.expected)
+
+ # Now remove individuals and exclusively BAD, and check that range is caught
+ bad["func_a"] = good["func_a"]
+ bad.pop("bad_func_a")
+ bad.pop("bad_func_b")
+
+ expected_cp = self.expected.copy()
+ expected_cp["bad_only_functions"] = False
+ expected_cp["bisect_results"] = {
+ "individuals": [],
+ "ranges": [["func_b", "func_c", "func_d"]],
+ }
+
+ self.run_check(good, bad, expected_cp)
+
+ def test_afdo_prof_state(self):
+ """Verifies that saved state is correct replication."""
+ temp_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, temp_dir, ignore_errors=True)
+
+ good = self.good_prof.copy()
+ bad = self.bad_prof.copy()
+ # add more functions to data
+ for x in range(400):
+ good["func_%d" % x] = ""
+ bad["func_%d" % x] = ""
+
+ fd_first, first_result = tempfile.mkstemp(dir=temp_dir)
+ os.close(fd_first)
+ fd_state, state_file = tempfile.mkstemp(dir=temp_dir)
+ os.close(fd_state)
+ self.run_check(
+ self.good_prof,
+ self.bad_prof,
+ self.expected,
+ state_file=state_file,
+ out_file=first_result,
+ )
+
+ fd_second, second_result = tempfile.mkstemp(dir=temp_dir)
+ os.close(fd_second)
+ completed_state_file = "%s.completed.%s" % (
+ state_file,
+ str(date.today()),
+ )
self.run_check(
+ self.good_prof,
+ self.bad_prof,
+ self.expected,
+ state_file=completed_state_file,
+ no_resume=False,
+ out_file=second_result,
+ )
+
+ with open(first_result) as f:
+ initial_run = json.load(f)
+ with open(second_result) as f:
+ loaded_run = json.load(f)
+ self.assertEqual(initial_run, loaded_run)
+
+ def test_exit_on_problem_status(self):
+ temp_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, temp_dir, ignore_errors=True)
+
+ fd_state, state_file = tempfile.mkstemp(dir=temp_dir)
+ os.close(fd_state)
+ with self.assertRaises(RuntimeError):
+ self.run_check(
+ self.good_prof,
+ self.bad_prof,
+ self.expected,
+ state_file=state_file,
+ extern_decider="problemstatus_external.sh",
+ )
+
+ def test_state_assumption(self):
+ def compare_runs(tmp_dir, first_ctr, second_ctr):
+ """Compares given prof versions between first and second run in test."""
+ first_prof = "%s/.first_run_%d" % (tmp_dir, first_ctr)
+ second_prof = "%s/.second_run_%d" % (tmp_dir, second_ctr)
+ with open(first_prof) as f:
+ first_prof_text = f.read()
+ with open(second_prof) as f:
+ second_prof_text = f.read()
+ self.assertEqual(first_prof_text, second_prof_text)
+
+ good_prof = {"func_a": ":1\n3: 3\n5: 7\n"}
+ bad_prof = {"func_a": ":2\n4: 4\n6: 8\n"}
+ # add some noise to the profiles; 15 is an arbitrary choice
+ for x in range(15):
+ func = "func_%d" % x
+ good_prof[func] = ":%d\n" % (x)
+ bad_prof[func] = ":%d\n" % (x + 1)
+ expected = {
+ "bisect_results": {"ranges": [], "individuals": ["func_a"]},
+ "good_only_functions": False,
+ "bad_only_functions": False,
+ }
+
+ # using a static temp dir rather than a dynamic one because these files are
+ # shared between the bash scripts and this Python test, and the arguments
+ # to the bash scripts are fixed by afdo_prof_analysis.py so it would be
+ # difficult to communicate dynamically generated directory to bash scripts
+ scripts_tmp_dir = "%s/afdo_test_tmp" % os.getcwd()
+ os.mkdir(scripts_tmp_dir)
+ self.addCleanup(shutil.rmtree, scripts_tmp_dir, ignore_errors=True)
+
+ # files used in the bash scripts used as external deciders below
+ # - count_file tracks the current number of calls to the script in total
+ # - local_count_file tracks the number of calls to the script without
+ # interruption
+ count_file = "%s/.count" % scripts_tmp_dir
+ local_count_file = "%s/.local_count" % scripts_tmp_dir
+
+ # runs through whole thing at once
+ initial_seed = self.run_check(
good_prof,
bad_prof,
expected,
- no_resume=no_resume_run,
- extern_decider='state_assumption_interrupt.sh',
- seed=seed)
- break
- except RuntimeError:
- # script was interrupted, so we restart local count
- os.remove(local_count_file)
- else:
- raise RuntimeError('Test failed -- took too many iterations')
-
- for initial_ctr in range(3): # initial runs unaffected by interruption
- compare_runs(scripts_tmp_dir, initial_ctr, initial_ctr)
-
- start = 3
- for ctr in range(start, num_calls):
- # second run counter incremented by 4 for each one first run is because
- # +2 for performing initial checks on good and bad profs each time
- # +1 for PROBLEM_STATUS run which causes error and restart
- compare_runs(scripts_tmp_dir, ctr, 6 + (ctr - start) * 4)
-
- def run_check(self,
- good_prof,
- bad_prof,
- expected,
- state_file=None,
- no_resume=True,
- out_file=None,
- extern_decider=None,
- seed=None):
-
- temp_dir = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, temp_dir, ignore_errors=True)
-
- good_prof_file = '%s/%s' % (temp_dir, 'good_prof.txt')
- bad_prof_file = '%s/%s' % (temp_dir, 'bad_prof.txt')
- good_prof_text = analysis.json_to_text(good_prof)
- bad_prof_text = analysis.json_to_text(bad_prof)
- with open(good_prof_file, 'w') as f:
- f.write(good_prof_text)
- with open(bad_prof_file, 'w') as f:
- f.write(bad_prof_text)
-
- dir_path = os.path.dirname(os.path.realpath(__file__)) # dir of this file
- external_script = '%s/%s' % (dir_path, extern_decider or 'e2e_external.sh')
-
- # FIXME: This test ideally shouldn't be writing to $PWD
- if state_file is None:
- state_file = '%s/afdo_analysis_state.json' % os.getcwd()
-
- def rm_state():
- try:
- os.unlink(state_file)
- except OSError:
- # Probably because the file DNE. That's fine.
- pass
-
- self.addCleanup(rm_state)
-
- actual = analysis.main(
- ObjectWithFields(
- good_prof=good_prof_file,
- bad_prof=bad_prof_file,
- external_decider=external_script,
- analysis_output_file=out_file or '/dev/null',
- state_file=state_file,
- no_resume=no_resume,
- remove_state_on_completion=False,
- seed=seed,
- ))
- actual_seed = actual.pop('seed') # nothing to check
- self.assertEqual(actual, expected)
- return actual_seed
-
-
-if __name__ == '__main__':
- unittest.main()
+ extern_decider="state_assumption_external.sh",
+ )
+ with open(count_file) as f:
+ num_calls = int(f.read())
+ os.remove(count_file) # reset counts for second run
+ finished_state_file = "afdo_analysis_state.json.completed.%s" % str(
+ date.today()
+ )
+ self.addCleanup(os.remove, finished_state_file)
+
+ # runs the same analysis but interrupted each iteration
+ for i in range(2 * num_calls + 1):
+ no_resume_run = i == 0
+ seed = initial_seed if no_resume_run else None
+ try:
+ self.run_check(
+ good_prof,
+ bad_prof,
+ expected,
+ no_resume=no_resume_run,
+ extern_decider="state_assumption_interrupt.sh",
+ seed=seed,
+ )
+ break
+ except RuntimeError:
+ # script was interrupted, so we restart local count
+ os.remove(local_count_file)
+ else:
+ raise RuntimeError("Test failed -- took too many iterations")
+
+ for initial_ctr in range(3): # initial runs unaffected by interruption
+ compare_runs(scripts_tmp_dir, initial_ctr, initial_ctr)
+
+ start = 3
+ for ctr in range(start, num_calls):
+ # second run counter incremented by 4 for each one first run is because
+ # +2 for performing initial checks on good and bad profs each time
+ # +1 for PROBLEM_STATUS run which causes error and restart
+ compare_runs(scripts_tmp_dir, ctr, 6 + (ctr - start) * 4)
+
+ def run_check(
+ self,
+ good_prof,
+ bad_prof,
+ expected,
+ state_file=None,
+ no_resume=True,
+ out_file=None,
+ extern_decider=None,
+ seed=None,
+ ):
+
+ temp_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, temp_dir, ignore_errors=True)
+
+ good_prof_file = "%s/%s" % (temp_dir, "good_prof.txt")
+ bad_prof_file = "%s/%s" % (temp_dir, "bad_prof.txt")
+ good_prof_text = analysis.json_to_text(good_prof)
+ bad_prof_text = analysis.json_to_text(bad_prof)
+ with open(good_prof_file, "w") as f:
+ f.write(good_prof_text)
+ with open(bad_prof_file, "w") as f:
+ f.write(bad_prof_text)
+
+ dir_path = os.path.dirname(
+ os.path.realpath(__file__)
+ ) # dir of this file
+ external_script = "%s/%s" % (
+ dir_path,
+ extern_decider or "e2e_external.sh",
+ )
+
+ # FIXME: This test ideally shouldn't be writing to $PWD
+ if state_file is None:
+ state_file = "%s/afdo_analysis_state.json" % os.getcwd()
+
+ def rm_state():
+ try:
+ os.unlink(state_file)
+ except OSError:
+ # Probably because the file DNE. That's fine.
+ pass
+
+ self.addCleanup(rm_state)
+
+ actual = analysis.main(
+ ObjectWithFields(
+ good_prof=good_prof_file,
+ bad_prof=bad_prof_file,
+ external_decider=external_script,
+ analysis_output_file=out_file or "/dev/null",
+ state_file=state_file,
+ no_resume=no_resume,
+ remove_state_on_completion=False,
+ seed=seed,
+ )
+ )
+ actual_seed = actual.pop("seed") # nothing to check
+ self.assertEqual(actual, expected)
+ return actual_seed
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/afdo_tools/bisection/afdo_prof_analysis_test.py b/afdo_tools/bisection/afdo_prof_analysis_test.py
index 245edc33..babfc021 100755
--- a/afdo_tools/bisection/afdo_prof_analysis_test.py
+++ b/afdo_tools/bisection/afdo_prof_analysis_test.py
@@ -1,12 +1,11 @@
#!/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.
"""Tests for afdo_prof_analysis."""
-from __future__ import print_function
import random
import io
@@ -16,139 +15,154 @@ from afdo_tools.bisection import afdo_prof_analysis as analysis
class AfdoProfAnalysisTest(unittest.TestCase):
- """Class for testing AFDO Profile Analysis"""
- bad_items = {'func_a': '1', 'func_b': '3', 'func_c': '5'}
- good_items = {'func_a': '2', 'func_b': '4', 'func_d': '5'}
- random.seed(13) # 13 is an arbitrary choice. just for consistency
- # add some extra info to make tests more reflective of real scenario
- for num in range(128):
- func_name = 'func_extra_%d' % num
- # 1/3 to both, 1/3 only to good, 1/3 only to bad
- rand_val = random.randint(1, 101)
- if rand_val < 67:
- bad_items[func_name] = 'test_data'
- if rand_val < 34 or rand_val >= 67:
- good_items[func_name] = 'test_data'
-
- analysis.random.seed(5) # 5 is an arbitrary choice. For consistent testing
-
- def test_text_to_json(self):
- test_data = io.StringIO('deflate_slow:87460059:3\n'
- ' 3: 24\n'
- ' 14: 54767\n'
- ' 15: 664 fill_window:22\n'
- ' 16: 661\n'
- ' 19: 637\n'
- ' 41: 36692 longest_match:36863\n'
- ' 44: 36692\n'
- ' 44.2: 5861\n'
- ' 46: 13942\n'
- ' 46.1: 14003\n')
- expected = {
- 'deflate_slow': ':87460059:3\n'
- ' 3: 24\n'
- ' 14: 54767\n'
- ' 15: 664 fill_window:22\n'
- ' 16: 661\n'
- ' 19: 637\n'
- ' 41: 36692 longest_match:36863\n'
- ' 44: 36692\n'
- ' 44.2: 5861\n'
- ' 46: 13942\n'
- ' 46.1: 14003\n'
- }
- actual = analysis.text_to_json(test_data)
- self.assertEqual(actual, expected)
- test_data.close()
-
- def test_text_to_json_empty_afdo(self):
- expected = {}
- actual = analysis.text_to_json('')
- self.assertEqual(actual, expected)
-
- def test_json_to_text(self):
- example_prof = {'func_a': ':1\ndata\n', 'func_b': ':2\nmore data\n'}
- expected_text = 'func_a:1\ndata\nfunc_b:2\nmore data\n'
- self.assertEqual(analysis.json_to_text(example_prof), expected_text)
-
- def test_bisect_profiles(self):
-
- # mock run of external script with arbitrarily-chosen bad profile vals
- # save_run specified and unused b/c afdo_prof_analysis.py
- # will call with argument explicitly specified
- # pylint: disable=unused-argument
- class DeciderClass(object):
- """Class for this tests's decider."""
-
- def run(self, prof, save_run=False):
- if '1' in prof['func_a'] or '3' in prof['func_b']:
- return analysis.StatusEnum.BAD_STATUS
- return analysis.StatusEnum.GOOD_STATUS
-
- results = analysis.bisect_profiles_wrapper(DeciderClass(), self.good_items,
- self.bad_items)
- self.assertEqual(results['individuals'], sorted(['func_a', 'func_b']))
- self.assertEqual(results['ranges'], [])
-
- def test_range_search(self):
-
- # arbitrarily chosen functions whose values in the bad profile constitute
- # a problematic pair
- # pylint: disable=unused-argument
- class DeciderClass(object):
- """Class for this tests's decider."""
-
- def run(self, prof, save_run=False):
- if '1' in prof['func_a'] and '3' in prof['func_b']:
- return analysis.StatusEnum.BAD_STATUS
- return analysis.StatusEnum.GOOD_STATUS
-
- # put the problematic combination in separate halves of the common funcs
- # so that non-bisecting search is invoked for its actual use case
- common_funcs = [func for func in self.good_items if func in self.bad_items]
- common_funcs.remove('func_a')
- common_funcs.insert(0, 'func_a')
- common_funcs.remove('func_b')
- common_funcs.append('func_b')
-
- problem_range = analysis.range_search(DeciderClass(), self.good_items,
- self.bad_items, common_funcs, 0,
- len(common_funcs))
-
- self.assertEqual(['func_a', 'func_b'], problem_range)
-
- def test_check_good_not_bad(self):
- func_in_good = 'func_c'
-
- # pylint: disable=unused-argument
- class DeciderClass(object):
- """Class for this tests's decider."""
-
- def run(self, prof, save_run=False):
- if func_in_good in prof:
- return analysis.StatusEnum.GOOD_STATUS
- return analysis.StatusEnum.BAD_STATUS
-
- self.assertTrue(
- analysis.check_good_not_bad(DeciderClass(), self.good_items,
- self.bad_items))
-
- def test_check_bad_not_good(self):
- func_in_bad = 'func_d'
-
- # pylint: disable=unused-argument
- class DeciderClass(object):
- """Class for this tests's decider."""
-
- def run(self, prof, save_run=False):
- if func_in_bad in prof:
- return analysis.StatusEnum.BAD_STATUS
- return analysis.StatusEnum.GOOD_STATUS
-
- self.assertTrue(
- analysis.check_bad_not_good(DeciderClass(), self.good_items,
- self.bad_items))
-
-
-if __name__ == '__main__':
- unittest.main()
+ """Class for testing AFDO Profile Analysis"""
+
+ bad_items = {"func_a": "1", "func_b": "3", "func_c": "5"}
+ good_items = {"func_a": "2", "func_b": "4", "func_d": "5"}
+ random.seed(13) # 13 is an arbitrary choice. just for consistency
+ # add some extra info to make tests more reflective of real scenario
+ for num in range(128):
+ func_name = "func_extra_%d" % num
+ # 1/3 to both, 1/3 only to good, 1/3 only to bad
+ rand_val = random.randint(1, 101)
+ if rand_val < 67:
+ bad_items[func_name] = "test_data"
+ if rand_val < 34 or rand_val >= 67:
+ good_items[func_name] = "test_data"
+
+ analysis.random.seed(5) # 5 is an arbitrary choice. For consistent testing
+
+ def test_text_to_json(self):
+ test_data = io.StringIO(
+ "deflate_slow:87460059:3\n"
+ " 3: 24\n"
+ " 14: 54767\n"
+ " 15: 664 fill_window:22\n"
+ " 16: 661\n"
+ " 19: 637\n"
+ " 41: 36692 longest_match:36863\n"
+ " 44: 36692\n"
+ " 44.2: 5861\n"
+ " 46: 13942\n"
+ " 46.1: 14003\n"
+ )
+ expected = {
+ "deflate_slow": ":87460059:3\n"
+ " 3: 24\n"
+ " 14: 54767\n"
+ " 15: 664 fill_window:22\n"
+ " 16: 661\n"
+ " 19: 637\n"
+ " 41: 36692 longest_match:36863\n"
+ " 44: 36692\n"
+ " 44.2: 5861\n"
+ " 46: 13942\n"
+ " 46.1: 14003\n"
+ }
+ actual = analysis.text_to_json(test_data)
+ self.assertEqual(actual, expected)
+ test_data.close()
+
+ def test_text_to_json_empty_afdo(self):
+ expected = {}
+ actual = analysis.text_to_json("")
+ self.assertEqual(actual, expected)
+
+ def test_json_to_text(self):
+ example_prof = {"func_a": ":1\ndata\n", "func_b": ":2\nmore data\n"}
+ expected_text = "func_a:1\ndata\nfunc_b:2\nmore data\n"
+ self.assertEqual(analysis.json_to_text(example_prof), expected_text)
+
+ def test_bisect_profiles(self):
+
+ # mock run of external script with arbitrarily-chosen bad profile vals
+ # save_run specified and unused b/c afdo_prof_analysis.py
+ # will call with argument explicitly specified
+ # pylint: disable=unused-argument
+ class DeciderClass(object):
+ """Class for this tests's decider."""
+
+ def run(self, prof, save_run=False):
+ if "1" in prof["func_a"] or "3" in prof["func_b"]:
+ return analysis.StatusEnum.BAD_STATUS
+ return analysis.StatusEnum.GOOD_STATUS
+
+ results = analysis.bisect_profiles_wrapper(
+ DeciderClass(), self.good_items, self.bad_items
+ )
+ self.assertEqual(results["individuals"], sorted(["func_a", "func_b"]))
+ self.assertEqual(results["ranges"], [])
+
+ def test_range_search(self):
+
+ # arbitrarily chosen functions whose values in the bad profile constitute
+ # a problematic pair
+ # pylint: disable=unused-argument
+ class DeciderClass(object):
+ """Class for this tests's decider."""
+
+ def run(self, prof, save_run=False):
+ if "1" in prof["func_a"] and "3" in prof["func_b"]:
+ return analysis.StatusEnum.BAD_STATUS
+ return analysis.StatusEnum.GOOD_STATUS
+
+ # put the problematic combination in separate halves of the common funcs
+ # so that non-bisecting search is invoked for its actual use case
+ common_funcs = [
+ func for func in self.good_items if func in self.bad_items
+ ]
+ common_funcs.remove("func_a")
+ common_funcs.insert(0, "func_a")
+ common_funcs.remove("func_b")
+ common_funcs.append("func_b")
+
+ problem_range = analysis.range_search(
+ DeciderClass(),
+ self.good_items,
+ self.bad_items,
+ common_funcs,
+ 0,
+ len(common_funcs),
+ )
+
+ self.assertEqual(["func_a", "func_b"], problem_range)
+
+ def test_check_good_not_bad(self):
+ func_in_good = "func_c"
+
+ # pylint: disable=unused-argument
+ class DeciderClass(object):
+ """Class for this tests's decider."""
+
+ def run(self, prof, save_run=False):
+ if func_in_good in prof:
+ return analysis.StatusEnum.GOOD_STATUS
+ return analysis.StatusEnum.BAD_STATUS
+
+ self.assertTrue(
+ analysis.check_good_not_bad(
+ DeciderClass(), self.good_items, self.bad_items
+ )
+ )
+
+ def test_check_bad_not_good(self):
+ func_in_bad = "func_d"
+
+ # pylint: disable=unused-argument
+ class DeciderClass(object):
+ """Class for this tests's decider."""
+
+ def run(self, prof, save_run=False):
+ if func_in_bad in prof:
+ return analysis.StatusEnum.BAD_STATUS
+ return analysis.StatusEnum.GOOD_STATUS
+
+ self.assertTrue(
+ analysis.check_bad_not_good(
+ DeciderClass(), self.good_items, self.bad_items
+ )
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/afdo_tools/bisection/state_assumption_external.sh b/afdo_tools/bisection/state_assumption_external.sh
index 1ad78ee2..a2076b0d 100755
--- a/afdo_tools/bisection/state_assumption_external.sh
+++ b/afdo_tools/bisection/state_assumption_external.sh
@@ -1,5 +1,5 @@
#!/bin/bash -eu
-# 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.
diff --git a/afdo_tools/bisection/state_assumption_interrupt.sh b/afdo_tools/bisection/state_assumption_interrupt.sh
index eba3a4b4..d1599d0b 100755
--- a/afdo_tools/bisection/state_assumption_interrupt.sh
+++ b/afdo_tools/bisection/state_assumption_interrupt.sh
@@ -1,5 +1,5 @@
#!/bin/bash -eu
-# 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.
diff --git a/afdo_tools/generate_afdo_from_tryjob.py b/afdo_tools/generate_afdo_from_tryjob.py
index 3ed578ea..e398f8a1 100755
--- a/afdo_tools/generate_afdo_from_tryjob.py
+++ b/afdo_tools/generate_afdo_from_tryjob.py
@@ -1,12 +1,11 @@
#!/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.
"""Given a tryjob and perf profile, generates an AFDO profile."""
-from __future__ import print_function
import argparse
import distutils.spawn
@@ -17,149 +16,162 @@ import subprocess
import sys
import tempfile
-_CREATE_LLVM_PROF = 'create_llvm_prof'
-_GS_PREFIX = 'gs://'
+
+_CREATE_LLVM_PROF = "create_llvm_prof"
+_GS_PREFIX = "gs://"
def _fetch_gs_artifact(remote_name, local_name):
- assert remote_name.startswith(_GS_PREFIX)
- subprocess.check_call(['gsutil', 'cp', remote_name, local_name])
+ assert remote_name.startswith(_GS_PREFIX)
+ subprocess.check_call(["gsutil", "cp", remote_name, local_name])
def _fetch_and_maybe_unpack(remote_name, local_name):
- unpackers = [
- ('.tar.bz2', ['tar', 'xaf']),
- ('.bz2', ['bunzip2']),
- ('.tar.xz', ['tar', 'xaf']),
- ('.xz', ['xz', '-d']),
- ]
-
- unpack_ext = None
- unpack_cmd = None
- for ext, unpack in unpackers:
- if remote_name.endswith(ext):
- unpack_ext, unpack_cmd = ext, unpack
- break
-
- download_to = local_name + unpack_ext if unpack_ext else local_name
- _fetch_gs_artifact(remote_name, download_to)
- if unpack_cmd is not None:
- print('Unpacking', download_to)
- subprocess.check_output(unpack_cmd + [download_to])
- assert os.path.exists(local_name)
+ unpackers = [
+ (".tar.bz2", ["tar", "xaf"]),
+ (".bz2", ["bunzip2"]),
+ (".tar.xz", ["tar", "xaf"]),
+ (".xz", ["xz", "-d"]),
+ ]
+
+ unpack_ext = None
+ unpack_cmd = None
+ for ext, unpack in unpackers:
+ if remote_name.endswith(ext):
+ unpack_ext, unpack_cmd = ext, unpack
+ break
+
+ download_to = local_name + unpack_ext if unpack_ext else local_name
+ _fetch_gs_artifact(remote_name, download_to)
+ if unpack_cmd is not None:
+ print("Unpacking", download_to)
+ subprocess.check_output(unpack_cmd + [download_to])
+ assert os.path.exists(local_name)
def _generate_afdo(perf_profile_loc, tryjob_loc, output_name):
- if perf_profile_loc.startswith(_GS_PREFIX):
- local_loc = 'perf.data'
- _fetch_and_maybe_unpack(perf_profile_loc, local_loc)
- perf_profile_loc = local_loc
-
- chrome_in_debug_loc = 'debug/opt/google/chrome/chrome.debug'
- debug_out = 'debug.tgz'
- _fetch_gs_artifact(os.path.join(tryjob_loc, 'debug.tgz'), debug_out)
-
- print('Extracting chrome.debug.')
- # This has tons of artifacts, and we only want Chrome; don't waste time
- # extracting the rest in _fetch_and_maybe_unpack.
- subprocess.check_call(['tar', 'xaf', 'debug.tgz', chrome_in_debug_loc])
-
- # Note that the AFDO tool *requires* a binary named `chrome` to be present if
- # we're generating a profile for chrome. It's OK for this to be split debug
- # information.
- os.rename(chrome_in_debug_loc, 'chrome')
-
- print('Generating AFDO profile.')
- subprocess.check_call([
- _CREATE_LLVM_PROF, '--out=' + output_name, '--binary=chrome',
- '--profile=' + perf_profile_loc
- ])
+ if perf_profile_loc.startswith(_GS_PREFIX):
+ local_loc = "perf.data"
+ _fetch_and_maybe_unpack(perf_profile_loc, local_loc)
+ perf_profile_loc = local_loc
+
+ chrome_in_debug_loc = "debug/opt/google/chrome/chrome.debug"
+ debug_out = "debug.tgz"
+ _fetch_gs_artifact(os.path.join(tryjob_loc, "debug.tgz"), debug_out)
+
+ print("Extracting chrome.debug.")
+ # This has tons of artifacts, and we only want Chrome; don't waste time
+ # extracting the rest in _fetch_and_maybe_unpack.
+ subprocess.check_call(["tar", "xaf", "debug.tgz", chrome_in_debug_loc])
+
+ # Note that the AFDO tool *requires* a binary named `chrome` to be present if
+ # we're generating a profile for chrome. It's OK for this to be split debug
+ # information.
+ os.rename(chrome_in_debug_loc, "chrome")
+
+ print("Generating AFDO profile.")
+ subprocess.check_call(
+ [
+ _CREATE_LLVM_PROF,
+ "--out=" + output_name,
+ "--binary=chrome",
+ "--profile=" + perf_profile_loc,
+ ]
+ )
def _abspath_or_gs_link(path):
- if path.startswith(_GS_PREFIX):
- return path
- return os.path.abspath(path)
+ if path.startswith(_GS_PREFIX):
+ return path
+ return os.path.abspath(path)
def _tryjob_arg(tryjob_arg):
- # Forward gs args through
- if tryjob_arg.startswith(_GS_PREFIX):
- return tryjob_arg
+ # Forward gs args through
+ if tryjob_arg.startswith(_GS_PREFIX):
+ return tryjob_arg
- # Clicking on the 'Artifacts' link gives us a pantheon link that's basically
- # a preamble and gs path.
- pantheon = 'https://pantheon.corp.google.com/storage/browser/'
- if tryjob_arg.startswith(pantheon):
- return _GS_PREFIX + tryjob_arg[len(pantheon):]
+ # Clicking on the 'Artifacts' link gives us a pantheon link that's basically
+ # a preamble and gs path.
+ pantheon = "https://pantheon.corp.google.com/storage/browser/"
+ if tryjob_arg.startswith(pantheon):
+ return _GS_PREFIX + tryjob_arg[len(pantheon) :]
- # Otherwise, only do things with a tryjob ID (e.g. R75-11965.0.0-b3648595)
- if not tryjob_arg.startswith('R'):
- raise ValueError('Unparseable tryjob arg; give a tryjob ID, pantheon '
- 'link, or gs:// link. Please see source for more.')
+ # Otherwise, only do things with a tryjob ID (e.g. R75-11965.0.0-b3648595)
+ if not tryjob_arg.startswith("R"):
+ raise ValueError(
+ "Unparseable tryjob arg; give a tryjob ID, pantheon "
+ "link, or gs:// link. Please see source for more."
+ )
- chell_path = 'chromeos-image-archive/chell-chrome-pfq-tryjob/'
- # ...And assume it's from chell, since that's the only thing we generate
- # profiles with today.
- return _GS_PREFIX + chell_path + tryjob_arg
+ chell_path = "chromeos-image-archive/chell-chrome-pfq-tryjob/"
+ # ...And assume it's from chell, since that's the only thing we generate
+ # profiles with today.
+ return _GS_PREFIX + chell_path + tryjob_arg
def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument(
- '--perf_profile',
- required=True,
- help='Path to our perf profile. Accepts either a gs:// path or local '
- 'filepath.')
- parser.add_argument(
- '--tryjob',
- required=True,
- type=_tryjob_arg,
- help="Path to our tryjob's artifacts. Accepts a gs:// path, pantheon "
- 'link, or tryjob ID, e.g. R75-11965.0.0-b3648595. In the last case, '
- 'the assumption is that you ran a chell-chrome-pfq-tryjob.')
- parser.add_argument(
- '-o',
- '--output',
- default='afdo.prof',
- help='Where to put the AFDO profile. Default is afdo.prof.')
- parser.add_argument(
- '-k',
- '--keep_artifacts_on_failure',
- action='store_true',
- help="Don't remove the tempdir on failure")
- args = parser.parse_args()
-
- if not distutils.spawn.find_executable(_CREATE_LLVM_PROF):
- sys.exit(_CREATE_LLVM_PROF + ' not found; are you in the chroot?')
-
- profile = _abspath_or_gs_link(args.perf_profile)
- afdo_output = os.path.abspath(args.output)
-
- initial_dir = os.getcwd()
- temp_dir = tempfile.mkdtemp(prefix='generate_afdo')
- success = True
- try:
- os.chdir(temp_dir)
- _generate_afdo(profile, args.tryjob, afdo_output)
-
- # The AFDO tooling is happy to generate essentially empty profiles for us.
- # Chrome's profiles are often 8+ MB; if we only see a small fraction of
- # that, something's off. 512KB was arbitrarily selected.
- if os.path.getsize(afdo_output) < 512 * 1024:
- raise ValueError('The AFDO profile is suspiciously small for Chrome. '
- 'Something might have gone wrong.')
- except:
- success = False
- raise
- finally:
- os.chdir(initial_dir)
-
- if success or not args.keep_artifacts_on_failure:
- shutil.rmtree(temp_dir, ignore_errors=True)
- else:
- print('Artifacts are available at', temp_dir)
-
-
-if __name__ == '__main__':
- sys.exit(main())
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--perf_profile",
+ required=True,
+ help="Path to our perf profile. Accepts either a gs:// path or local "
+ "filepath.",
+ )
+ parser.add_argument(
+ "--tryjob",
+ required=True,
+ type=_tryjob_arg,
+ help="Path to our tryjob's artifacts. Accepts a gs:// path, pantheon "
+ "link, or tryjob ID, e.g. R75-11965.0.0-b3648595. In the last case, "
+ "the assumption is that you ran a chell-chrome-pfq-tryjob.",
+ )
+ parser.add_argument(
+ "-o",
+ "--output",
+ default="afdo.prof",
+ help="Where to put the AFDO profile. Default is afdo.prof.",
+ )
+ parser.add_argument(
+ "-k",
+ "--keep_artifacts_on_failure",
+ action="store_true",
+ help="Don't remove the tempdir on failure",
+ )
+ args = parser.parse_args()
+
+ if not distutils.spawn.find_executable(_CREATE_LLVM_PROF):
+ sys.exit(_CREATE_LLVM_PROF + " not found; are you in the chroot?")
+
+ profile = _abspath_or_gs_link(args.perf_profile)
+ afdo_output = os.path.abspath(args.output)
+
+ initial_dir = os.getcwd()
+ temp_dir = tempfile.mkdtemp(prefix="generate_afdo")
+ success = True
+ try:
+ os.chdir(temp_dir)
+ _generate_afdo(profile, args.tryjob, afdo_output)
+
+ # The AFDO tooling is happy to generate essentially empty profiles for us.
+ # Chrome's profiles are often 8+ MB; if we only see a small fraction of
+ # that, something's off. 512KB was arbitrarily selected.
+ if os.path.getsize(afdo_output) < 512 * 1024:
+ raise ValueError(
+ "The AFDO profile is suspiciously small for Chrome. "
+ "Something might have gone wrong."
+ )
+ except:
+ success = False
+ raise
+ finally:
+ os.chdir(initial_dir)
+
+ if success or not args.keep_artifacts_on_failure:
+ shutil.rmtree(temp_dir, ignore_errors=True)
+ else:
+ print("Artifacts are available at", temp_dir)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/afdo_tools/run_afdo_tryjob.py b/afdo_tools/run_afdo_tryjob.py
index e14cd918..013e10c6 100755
--- a/afdo_tools/run_afdo_tryjob.py
+++ b/afdo_tools/run_afdo_tryjob.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.
@@ -49,7 +49,6 @@ If you provide neither --use_afdo_generation_stage nor
since it's safer.
"""
-from __future__ import print_function
import argparse
import collections
@@ -60,112 +59,124 @@ import time
def main():
- parser = argparse.ArgumentParser(
- description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
- parser.add_argument(
- '--force_no_patches',
- action='store_true',
- help='Run even if no patches are provided')
- parser.add_argument(
- '--tag_profiles_with_current_time',
- action='store_true',
- help='Perf profile names will have the current time added to them.')
- parser.add_argument(
- '--use_afdo_generation_stage',
- action='store_true',
- help='Perf profiles will be automatically converted to AFDO profiles.')
- parser.add_argument(
- '-g',
- '--patch',
- action='append',
- default=[],
- help='A patch to add to the AFDO run')
- parser.add_argument(
- '-n',
- '--dry_run',
- action='store_true',
- help='Just print the command that would be run')
- args = parser.parse_args()
-
- dry_run = args.dry_run
- force_no_patches = args.force_no_patches
- tag_profiles_with_current_time = args.tag_profiles_with_current_time
- use_afdo_generation_stage = args.use_afdo_generation_stage
- user_patches = args.patch
-
- if tag_profiles_with_current_time and use_afdo_generation_stage:
- raise ValueError("You can't tag profiles with the time + have "
- 'afdo-generate')
-
- if not tag_profiles_with_current_time and not use_afdo_generation_stage:
- print('Neither current_time nor afdo_generate asked for. Assuming you '
- 'prefer current time tagging.')
- print('You have 5 seconds to cancel and try again.')
- print()
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument(
+ "--force_no_patches",
+ action="store_true",
+ help="Run even if no patches are provided",
+ )
+ parser.add_argument(
+ "--tag_profiles_with_current_time",
+ action="store_true",
+ help="Perf profile names will have the current time added to them.",
+ )
+ parser.add_argument(
+ "--use_afdo_generation_stage",
+ action="store_true",
+ help="Perf profiles will be automatically converted to AFDO profiles.",
+ )
+ parser.add_argument(
+ "-g",
+ "--patch",
+ action="append",
+ default=[],
+ help="A patch to add to the AFDO run",
+ )
+ parser.add_argument(
+ "-n",
+ "--dry_run",
+ action="store_true",
+ help="Just print the command that would be run",
+ )
+ args = parser.parse_args()
+
+ dry_run = args.dry_run
+ force_no_patches = args.force_no_patches
+ tag_profiles_with_current_time = args.tag_profiles_with_current_time
+ use_afdo_generation_stage = args.use_afdo_generation_stage
+ user_patches = args.patch
+
+ if tag_profiles_with_current_time and use_afdo_generation_stage:
+ raise ValueError(
+ "You can't tag profiles with the time + have " "afdo-generate"
+ )
+
+ if not tag_profiles_with_current_time and not use_afdo_generation_stage:
+ print(
+ "Neither current_time nor afdo_generate asked for. Assuming you "
+ "prefer current time tagging."
+ )
+ print("You have 5 seconds to cancel and try again.")
+ print()
+ if not dry_run:
+ time.sleep(5)
+ tag_profiles_with_current_time = True
+
+ patches = [
+ # Send profiles to localmirror instead of chromeos-prebuilt. This should
+ # always be done, since sending profiles into production is bad. :)
+ # https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/1436158
+ 1436158,
+ # Force profile generation. Otherwise, we'll decide to not spawn off the
+ # perf hwtests.
+ # https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1313291
+ 1313291,
+ ]
+
+ if tag_profiles_with_current_time:
+ # Tags the profiles with the current time of day. As detailed in the
+ # docstring, this is desirable unless you're sure that this is the only
+ # experimental profile that will be generated today.
+ # https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/1436157
+ patches.append(1436157)
+
+ if use_afdo_generation_stage:
+ # Make the profile generation stage look in localmirror, instead of having
+ # it look in chromeos-prebuilt. Without this, we'll never upload
+ # chrome.debug or try to generate an AFDO profile.
+ # https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1436583
+ patches.append(1436583)
+
+ if not user_patches and not force_no_patches:
+ raise ValueError(
+ "No patches given; pass --force_no_patches to force a " "tryjob"
+ )
+
+ for patch in user_patches:
+ # We accept two formats. Either a URL that ends with a number, or a number.
+ if patch.startswith("http"):
+ patch = patch.split("/")[-1]
+ patches.append(int(patch))
+
+ count = collections.Counter(patches)
+ too_many = [k for k, v in count.items() if v > 1]
+ if too_many:
+ too_many.sort()
+ raise ValueError(
+ "Patch(es) asked for application more than once: %s" % too_many
+ )
+
+ args = [
+ "cros",
+ "tryjob",
+ ]
+
+ for patch in patches:
+ args += ["-g", str(patch)]
+
+ args += [
+ "--nochromesdk",
+ "--hwtest",
+ "chell-chrome-pfq-tryjob",
+ ]
+
+ print(" ".join(pipes.quote(a) for a in args))
if not dry_run:
- time.sleep(5)
- tag_profiles_with_current_time = True
-
- patches = [
- # Send profiles to localmirror instead of chromeos-prebuilt. This should
- # always be done, since sending profiles into production is bad. :)
- # https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/1436158
- 1436158,
- # Force profile generation. Otherwise, we'll decide to not spawn off the
- # perf hwtests.
- # https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1313291
- 1313291,
- ]
-
- if tag_profiles_with_current_time:
- # Tags the profiles with the current time of day. As detailed in the
- # docstring, this is desirable unless you're sure that this is the only
- # experimental profile that will be generated today.
- # https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/1436157
- patches.append(1436157)
-
- if use_afdo_generation_stage:
- # Make the profile generation stage look in localmirror, instead of having
- # it look in chromeos-prebuilt. Without this, we'll never upload
- # chrome.debug or try to generate an AFDO profile.
- # https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1436583
- patches.append(1436583)
-
- if not user_patches and not force_no_patches:
- raise ValueError('No patches given; pass --force_no_patches to force a '
- 'tryjob')
-
- for patch in user_patches:
- # We accept two formats. Either a URL that ends with a number, or a number.
- if patch.startswith('http'):
- patch = patch.split('/')[-1]
- patches.append(int(patch))
-
- count = collections.Counter(patches)
- too_many = [k for k, v in count.items() if v > 1]
- if too_many:
- too_many.sort()
- raise ValueError(
- 'Patch(es) asked for application more than once: %s' % too_many)
-
- args = [
- 'cros',
- 'tryjob',
- ]
-
- for patch in patches:
- args += ['-g', str(patch)]
-
- args += [
- '--nochromesdk',
- '--hwtest',
- 'chell-chrome-pfq-tryjob',
- ]
-
- print(' '.join(pipes.quote(a) for a in args))
- if not dry_run:
- sys.exit(subprocess.call(args))
-
-
-if __name__ == '__main__':
- main()
+ sys.exit(subprocess.call(args))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/afdo_tools/update_kernel_afdo b/afdo_tools/update_kernel_afdo
index ff0ab224..9e4d645d 100755
--- a/afdo_tools/update_kernel_afdo
+++ b/afdo_tools/update_kernel_afdo
@@ -1,5 +1,5 @@
#!/bin/bash
-# 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.
@@ -9,37 +9,74 @@
#
USAGE="
-Usage: $(basename $0) [main|beta|stable|all] [--help]
+Usage: $(basename "$0") [--noupload|-upload] [main|beta|stable|all] [--help]
Description:
The script takes one optional argument which is the channel where we want
to update the kernel afdo and creates a commit (or commits with \"all\"
channels) in the corresponding branch.
No arguments defaults to \"all\".
- Follow the prompt to submit the changes.
+ Follow the prompt to upload the changes.
NO CLEAN-UP NEEDED. The script ignores any local changes and keeps
the current branch unchanged.
+
+ Args:
+ --help Show this help.
+ --upload Upload CLs when the update succeeded (default).
+ --noupload Do not upload CLs. Instead, print the upload commands.
+ main|beta|stable Update metadata only on the specified channel.
"
set -eu
set -o pipefail
-GS_BASE=gs://chromeos-prebuilt/afdo-job/vetted/kernel
-KVERS="4.4 4.14 4.19 5.4"
+AMD_GS_BASE=gs://chromeos-prebuilt/afdo-job/vetted/kernel
+ARM_GS_BASE=gs://chromeos-prebuilt/afdo-job/vetted/kernel/arm
+AMD_KVERS="4.14 4.19 5.4 5.10"
+ARM_KVERS="5.15"
failed_channels=""
# Add skipped chrome branches in ascending order here.
SKIPPED_BRANCHES="95"
+# NOTE: We enable/disable kernel AFDO starting from a particular branch.
+# For example if we want to enable kernel AFDO in 5.15, first, we do it
+# in main. In this case we want to disable it in beta and stable branches.
+# The second scenario is when we want to disable kernel AFDO (when all devices
+# move to kernelnext and there are no new profiles from the field). In this
+# case we disable AFDO in main but still keep it live in beta and stable.
+declare -A SKIPPED_KVERS_IN_BRANCHES
+# In SKIPPED_KVERS_IN_BRANCHES
+# - key is a branch number string;
+# - value is the list of kernels separated by space.
+# Example: SKIPPED_KVERS_IN_BRANCHES["105"]="4.4 4.14"
+
+# b/223115767. In M-100 there are no new profiles in 5.10. And AFDO is not
+# enabled on any 5.10 board in M-100 either.
+SKIPPED_KVERS_IN_BRANCHES["100"]="5.10"
+
script_dir=$(dirname "$0")
tc_utils_dir="${script_dir}/.."
metadata_dir="${tc_utils_dir}/afdo_metadata"
-outfile="$(realpath --relative-to="${tc_utils_dir}" \
+amd_outfile="$(realpath --relative-to="${tc_utils_dir}" \
"${metadata_dir}"/kernel_afdo.json)"
+arm_outfile="$(realpath --relative-to="${tc_utils_dir}" \
+ "${metadata_dir}"/kernel_arm_afdo.json)"
# Convert toolchain_utils into the absolute path.
-abs_tc_utils_dir="$(realpath ${tc_utils_dir})"
+abs_tc_utils_dir="$(realpath "${tc_utils_dir}")"
# Check profiles uploaded within the last week.
expected_time=$(date +%s -d "week ago")
+# Upload CLs on success.
+upload_cl=true
+
+ARCHS="amd arm"
+declare -A arch_gsbase arch_kvers arch_outfile
+arch_gsbase["amd"]="${AMD_GS_BASE}"
+arch_gsbase["arm"]="${ARM_GS_BASE}"
+arch_kvers["amd"]="${AMD_KVERS}"
+arch_kvers["arm"]="${ARM_KVERS}"
+arch_outfile["amd"]="${amd_outfile}"
+arch_outfile["arm"]="${arm_outfile}"
declare -A branch branch_number commit
remote_repo=$(git -C "${tc_utils_dir}" remote)
@@ -47,10 +84,9 @@ canary_ref="refs/heads/main"
# Read the last two release-Rxx from remote branches
# and assign them to stable_ref and beta_ref.
# sort -V is the version sort which puts R100 after R99.
-last_branches=$(git -C "${tc_utils_dir}" ls-remote -h "${remote_repo}" \
- release-R\* | cut -f2 | sort -V | tail -n 2)
# We need `echo` to convert newlines into spaces for read.
-read stable_ref beta_ref <<< $(echo ${last_branches})
+read -r stable_ref beta_ref <<< "$(git -C "${tc_utils_dir}" ls-remote -h \
+ "${remote_repo}" release-R\* | cut -f2 | sort -V | tail -n 2 | paste -s)"
# Branch names which start from release-R.
branch["beta"]=${beta_ref##*/}
branch["stable"]=${stable_ref##*/}
@@ -62,33 +98,53 @@ branch_number["stable"]=$(echo "${branch["stable"]}" | \
branch_number["beta"]=$(echo "${branch["beta"]}" | \
sed -n -e "s/^release-R\([0-9][0-9]*\).*$/\1/p")
branch_number["canary"]="$((branch_number[beta] + 1))"
-for skipped_branch in $SKIPPED_BRANCHES ; do
- if [[ ${branch_number["canary"]} == $skipped_branch ]] ; then
+for skipped_branch in ${SKIPPED_BRANCHES} ; do
+ if [[ ${branch_number["canary"]} == "${skipped_branch}" ]] ; then
((branch_number[canary]++))
fi
done
# Without arguments the script updates all branches.
-channels=${1:-"all"}
-case "${channels}" in
+channels=""
+for arg in "$@"
+do
+ case "${arg}" in
stable | canary | beta )
+ channels="${channels} ${arg}"
;;
main )
- channels="canary"
+ channels="${channels} canary"
;;
all )
channels="canary beta stable"
;;
+ --noupload | --no-upload)
+ upload_cl=false
+ ;;
+ --upload)
+ upload_cl=true
+ ;;
--help | help | -h )
- echo "$USAGE"
+ echo "${USAGE}"
exit 0
;;
- * )
- echo "Channel \"${channels}\" is not supported.
+ -*)
+ echo "Option \"${arg}\" is not supported." >&2
+ echo "${USAGE}"
+ exit 1
+ ;;
+ *)
+ echo "Channel \"${arg}\" is not supported.
Must be main (or canary), beta, stable or all." >&2
- echo "$USAGE"
+ echo "${USAGE}"
exit 1
-esac
+ esac
+done
+
+if [[ -z "${channels}" ]]
+then
+ channels="canary beta stable"
+fi
# Fetch latest branches.
git -C "${tc_utils_dir}" fetch "${remote_repo}"
@@ -99,11 +155,20 @@ echo "-> Working in ${worktree_dir}"
# This way we don't need to clean-up and sync toolchain_utils before the
# change. Neither we should care about clean-up after the submit.
git -C "${tc_utils_dir}" worktree add --detach "${worktree_dir}"
-trap "git -C ${abs_tc_utils_dir} worktree remove ${worktree_dir}" EXIT
-cd "${worktree_dir}"
+trap 'git -C "${abs_tc_utils_dir}" worktree remove -f "${worktree_dir}"' EXIT
+pushd "${worktree_dir}"
for channel in ${channels}
do
+ set +u
+ if [[ -n "${commit[${channel}]}" ]]
+ then
+ echo "Skipping channel ${channel} which already has commit\
+ ${commit[${channel}]}."
+ continue
+ fi
+ set -u
+
errs=""
successes=0
curr_branch_number=${branch_number[${channel}]}
@@ -111,87 +176,124 @@ do
echo
echo "Checking \"${channel}\" channel..."
echo "branch_number=${curr_branch_number} branch=${curr_branch}"
- json="{"
- sep=""
- for kver in $KVERS
+
+ git reset --hard HEAD
+ git checkout "${remote_repo}/${curr_branch}"
+
+ for arch in ${ARCHS}
do
- # Sort the gs output by timestamp (default ordering is by name, so
- # R86-13310.3-1594633089.gcov.xz goes after R86-13310.18-1595237847.gcov.xz)
- latest=$(gsutil.py ls -l "$GS_BASE/$kver/" | sort -k2 | \
- grep "R${curr_branch_number}" | tail -1 || true)
- if [[ -z "$latest" && "${channel}" != "stable" ]]
- then
- # if no profiles exist for the current branch, try the previous branch
- latest=$(gsutil.py ls -l "$GS_BASE/$kver/" | sort -k2 | \
- grep "R$((curr_branch_number - 1))" | tail -1)
- fi
+ json="{"
+ sep=""
+ for kver in ${arch_kvers[${arch}]}
+ do
+ # Skip kernels disabled in this branch.
+ skipped=false
+ for skipped_branch in "${!SKIPPED_KVERS_IN_BRANCHES[@]}"
+ do
+ if [[ ${curr_branch_number} == "${skipped_branch}" ]]
+ then
+ # Current branch is in the keys of SKIPPED_KVERS_IN_BRANCHES.
+ # Now lets check if $kver is in the list.
+ for skipped_kver in ${SKIPPED_KVERS_IN_BRANCHES[${skipped_branch}]}
+ do
+ if [[ ${kver} == "${skipped_kver}" ]]
+ then
+ skipped=true
+ break
+ fi
+ done
+ fi
+ done
+ if ${skipped}
+ then
+ echo "${kver} is skipped in branch ${curr_branch_number}. Skip it."
+ continue
+ fi
+ # Sort the gs output by timestamp, default ordering is by name. So
+ # R86-13310.3-1594633089.gcov.xz goes after
+ # R86-13310.18-1595237847.gcov.xz.
+ latest=$(gsutil.py ls -l "${arch_gsbase[${arch}]}/${kver}/" | sort -k2 | \
+ grep "R${curr_branch_number}" | tail -1 || true)
+ if [[ -z "${latest}" && "${channel}" != "stable" ]]
+ then
+ # if no profiles exist for the current branch, try the previous branch
+ latest=$(gsutil.py ls -l "${arch_gsbase[${arch}]}/${kver}/" | \
+ sort -k2 | grep "R$((curr_branch_number - 1))" | tail -1)
+ fi
- # Verify that the file has the expected date.
- file_time=$(echo "$latest" | awk '{print $2}')
- file_time_unix=$(date +%s -d "$file_time")
- if [ $file_time_unix -lt $expected_time ]
- then
- expected=$(env TZ=UTC date +%Y-%m-%dT%H:%M:%SZ -d @$expected_time)
- echo "Wrong date for $kver: $file_time is before $expected" >&2
- errs="$errs $kver"
- continue
- fi
+ # Verify that the file has the expected date.
+ file_time=$(echo "${latest}" | awk '{print $2}')
+ file_time_unix=$(date +%s -d "${file_time}")
+ if [ "${file_time_unix}" -lt "${expected_time}" ]
+ then
+ expected=$(env TZ=UTC date +%Y-%m-%dT%H:%M:%SZ -d @"${expected_time}")
+ echo "Wrong date for ${kver}: ${file_time} is before ${expected}" >&2
+ errs="${errs} ${kver}"
+ continue
+ fi
- # Generate JSON.
- json_kver=$(echo "$kver" | tr . _)
- # b/147370213 (migrating profiles from gcov format) may result in the
- # pattern below no longer doing the right thing.
- name=$(echo "$latest" | sed 's%.*/\(.*\)\.gcov.*%\1%')
- json=$(cat <<EOT
-$json$sep
- "chromeos-kernel-$json_kver": {
- "name": "$name"
+ # Generate JSON.
+ json_kver=$(echo "${kver}" | tr . _)
+ # b/147370213 (migrating profiles from gcov format) may result in the
+ # pattern below no longer doing the right thing.
+ name="$(basename "${latest%.gcov.*}")"
+ # Skip kernels with no AFDO support in the current channel.
+ if [[ "${name}" == "" ]]
+ then
+ continue
+ fi
+ json=$(cat <<EOT
+${json}${sep}
+ "chromeos-kernel-${json_kver}": {
+ "name": "${name}"
}
EOT
- )
- sep=","
- successes=$((successes + 1))
- done
+ )
+ sep=","
+ successes=$((successes + 1))
+ done # kvers loop
- # If we did not succeed for any kvers, exit now.
- if [[ $successes -eq 0 ]]
- then
- echo "error: AFDO profiles out of date for all kernel versions" >&2
- failed_channels="${failed_channels} ${channel}"
- continue
- fi
+ # If we did not succeed for any kvers, exit now.
+ if [[ ${successes} -eq 0 ]]
+ then
+ echo "error: AFDO profiles out of date for all kernel versions" >&2
+ failed_channels="${failed_channels} ${channel}"
+ continue
+ fi
- git reset --hard HEAD
- echo git checkout "${remote_repo}/${curr_branch}"
- git checkout "${remote_repo}/${curr_branch}"
+ # Write new JSON file.
+ # Don't use `echo` since `json` might have esc characters in it.
+ printf "%s\n}\n" "${json}" > "${arch_outfile[${arch}]}"
- # Write new JSON file.
- # Don't use `echo` since `json` might have esc characters in it.
- printf "%s\n}\n" "$json" > "$outfile"
+ # If no changes were made, say so.
+ outdir=$(dirname "${arch_outfile[${arch}]}")
+ shortstat=$(cd "${outdir}" &&\
+ git status --short "$(basename "${arch_outfile[${arch}]}")")
+ [ -z "${shortstat}" ] &&\
+ echo "$(basename "${arch_outfile[${arch}]}") is up to date." \
+ && continue
- # If no changes were made, say so.
- outdir=$(dirname "$outfile")
- shortstat=$(cd "$outdir" && git status --short $(basename "$outfile"))
- [ -z "$shortstat" ] && echo $(basename "$outfile")" is up to date." \
- && continue
+ # If we had any errors, warn about them.
+ if [[ -n "${errs}" ]]
+ then
+ echo "warning: failed to update ${errs} in ${channel}" >&2
+ failed_channels="${failed_channels} ${channel}"
+ continue
+ fi
- # If we had any errors, warn about them.
- if [[ -n "$errs" ]]
- then
- echo "warning: failed to update $errs in ${channel}" >&2
- failed_channels="${failed_channels} ${channel}"
- continue
- fi
+ git add "${arch_outfile[${arch}]}"
+ done # ARCHS loop
- git add afdo_metadata/kernel_afdo.json
case "${channel}" in
canary )
- commit_contents="afdo_metadata: Publish the new kernel profiles
-
-Update chromeos-kernel-4_4
-Update chromeos-kernel-4_14
-Update chromeos-kernel-4_19
-Update chromeos-kernel-5_4
+ commit_contents=$'afdo_metadata: Publish the new kernel profiles\n\n'
+ for arch in ${ARCHS} ; do
+ for kver in ${arch_kvers[${arch}]} ; do
+ commit_contents="${commit_contents}Update ${arch} profile on\
+ chromeos-kernel-${kver}"$'\n'
+ done
+ done
+ commit_contents="${commit_contents}
BUG=None
TEST=Verified in kernel-release-afdo-verify-orchestrator"
@@ -215,20 +317,30 @@ TEST=Verified in kernel-release-afdo-verify-orchestrator"
commit[${channel}]=$(git -C "${worktree_dir}" rev-parse HEAD)
done
+popd
echo
# Array size check doesn't play well with the unbound variable option.
set +u
if [[ ${#commit[@]} -gt 0 ]]
then
set -u
- echo "The change is applied in ${!commit[@]}."
- echo "Run these commands to submit the change:"
- echo
- for channel in ${!commit[@]}
- do
- echo -e "\tgit -C ${tc_utils_dir} push ${remote_repo} \
-${commit[${channel}]}:refs/for/${branch[${channel}]}"
- done
+ echo "The change is applied in ${!commit[*]}."
+ if ${upload_cl}
+ then
+ for channel in "${!commit[@]}"
+ do
+ git -C "${tc_utils_dir}" push "${remote_repo}" \
+ "${commit[${channel}]}:refs/for/${branch[${channel}]}"
+ done
+ else
+ echo "Run these commands to upload the change:"
+ echo
+ for channel in "${!commit[@]}"
+ do
+ echo -e "\tgit -C ${tc_utils_dir} push ${remote_repo} \
+ ${commit[${channel}]}:refs/for/${branch[${channel}]}"
+ done
+ fi
# Report failed channels.
if [[ -n "${failed_channels}" ]]