| #!/usr/bin/env python3 |
| |
| # SPDX-License-Identifier: Apache-2.0 |
| """ |
| This script help you to compare footprint results with previous commits in git. |
| If you don't have a git repository, it will compare your current tree |
| against the last release results. |
| To run it you need to set up the same environment as twister. |
| The scripts take 2 optional args COMMIT and BASE_COMMIT, which tell the scripts |
| which commit to use as current commit and as base for comparing, respectively. |
| The script can take any SHA commit recognized for git. |
| |
| COMMIT is the commit to compare against BASE_COMMIT. |
| Default |
| current working directory if we have changes in git tree or we don't have git. |
| HEAD in any other case. |
| BASE_COMMIT is the commit used as base to compare results. |
| Default: |
| twister_last_release.csv if we don't have git tree. |
| HEAD is we have changes in the working tree. |
| HEAD~1 if we don't have changes and we have default COMMIT. |
| COMMIT~1 if we have a valid COMMIT. |
| |
| """ |
| |
| import argparse |
| import os |
| import csv |
| import subprocess |
| import logging |
| import tempfile |
| import shutil |
| |
| if "ZEPHYR_BASE" not in os.environ: |
| logging.error("$ZEPHYR_BASE environment variable undefined.\n") |
| exit(1) |
| |
| logger = None |
| GIT_ENABLED = False |
| RELEASE_DATA = 'twister_last_release.csv' |
| |
| def is_git_enabled(): |
| global GIT_ENABLED |
| proc = subprocess.Popen('git rev-parse --is-inside-work-tree', |
| stdout=subprocess.PIPE, |
| cwd=os.environ.get('ZEPHYR_BASE'), shell=True) |
| if proc.wait() != 0: |
| GIT_ENABLED = False |
| |
| GIT_ENABLED = True |
| |
| def init_logs(): |
| global logger |
| log_lev = os.environ.get('LOG_LEVEL', None) |
| level = logging.INFO |
| if log_lev == "DEBUG": |
| level = logging.DEBUG |
| elif log_lev == "ERROR": |
| level = logging.ERROR |
| |
| console = logging.StreamHandler() |
| format = logging.Formatter('%(levelname)-8s: %(message)s') |
| console.setFormatter(format) |
| logger = logging.getLogger('') |
| logger.addHandler(console) |
| logger.setLevel(level) |
| |
| logging.debug("Log init completed") |
| |
| def parse_args(): |
| parser = argparse.ArgumentParser( |
| description="Compare footprint apps RAM and ROM sizes. Note: " |
| "To run it you need to set up the same environment as twister.", |
| allow_abbrev=False) |
| parser.add_argument('-b', '--base-commit', default=None, |
| help="Commit ID to use as base for footprint " |
| "compare. Default is parent current commit." |
| " or twister_last_release.csv if we don't have git.") |
| parser.add_argument('-c', '--commit', default=None, |
| help="Commit ID to use compare footprint against base. " |
| "Default is HEAD or working tree.") |
| return parser.parse_args() |
| |
| def get_git_commit(commit): |
| commit_id = None |
| proc = subprocess.Popen('git rev-parse %s' % commit, stdout=subprocess.PIPE, |
| cwd=os.environ.get('ZEPHYR_BASE'), shell=True) |
| if proc.wait() == 0: |
| commit_id = proc.stdout.read().decode("utf-8").strip() |
| return commit_id |
| |
| def sanity_results_filename(commit=None, cwd=os.environ.get('ZEPHYR_BASE')): |
| if not commit: |
| file_name = "tmp.csv" |
| else: |
| if commit == RELEASE_DATA: |
| file_name = RELEASE_DATA |
| else: |
| file_name = "%s.csv" % commit |
| |
| return os.path.join(cwd,'scripts', 'sanity_chk', file_name) |
| |
| def git_checkout(commit, cwd=os.environ.get('ZEPHYR_BASE')): |
| proc = subprocess.Popen('git diff --quiet', stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, cwd=cwd, shell=True) |
| if proc.wait() != 0: |
| raise Exception("Cannot continue, you have unstaged changes in your working tree") |
| |
| proc = subprocess.Popen('git reset %s --hard' % commit, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| cwd=cwd, shell=True) |
| if proc.wait() == 0: |
| return True |
| else: |
| logger.error(proc.stdout.read()) |
| return False |
| |
| def run_sanity_footprint(commit=None, cwd=os.environ.get('ZEPHYR_BASE'), |
| output_file=None): |
| if not output_file: |
| output_file = sanity_results_filename(commit) |
| cmd = '/bin/bash -c "source ./zephyr-env.sh && twister' |
| cmd += ' +scripts/sanity_chk/sanity_compare.args -o %s"' % output_file |
| logger.debug('Sanity (%s) %s' %(commit, cmd)) |
| |
| proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, |
| cwd=cwd, shell=True) |
| output,_ = proc.communicate() |
| if proc.wait() == 0: |
| logger.debug(output) |
| return True |
| |
| logger.error("Couldn't build footprint apps in commit %s" % commit) |
| logger.error(output) |
| raise Exception("Couldn't build footprint apps in commit %s" % commit) |
| |
| def run_footprint_build(commit=None): |
| logging.debug("footprint build for %s" % commit) |
| if not commit: |
| run_sanity_footprint() |
| else: |
| cmd = "git clone --no-hardlinks %s" % os.environ.get('ZEPHYR_BASE') |
| tmp_location = os.path.join(tempfile.gettempdir(), |
| os.path.basename(os.environ.get('ZEPHYR_BASE'))) |
| if os.path.exists(tmp_location): |
| shutil.rmtree(tmp_location) |
| logging.debug("cloning into %s" % tmp_location) |
| proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| cwd=tempfile.gettempdir(), shell=True) |
| if proc.wait() == 0: |
| if git_checkout(commit, tmp_location): |
| run_sanity_footprint(commit, tmp_location) |
| else: |
| logger.error(proc.stdout.read()) |
| shutil.rmtree(tmp_location, ignore_errors=True) |
| return True |
| |
| def read_sanity_report(filename): |
| data = [] |
| with open(filename) as fp: |
| tmp = csv.DictReader(fp) |
| for row in tmp: |
| data.append(row) |
| return data |
| |
| def get_footprint_results(commit=None): |
| sanity_file = sanity_results_filename(commit) |
| if (not os.path.exists(sanity_file) or not commit) and commit != RELEASE_DATA: |
| run_footprint_build(commit) |
| |
| return read_sanity_report(sanity_file) |
| |
| def tree_changes(): |
| proc = subprocess.Popen('git diff --quiet', stdout=subprocess.PIPE, |
| cwd=os.environ.get('ZEPHYR_BASE'), shell=True) |
| if proc.wait() != 0: |
| return True |
| return False |
| |
| def get_default_current_commit(): |
| if tree_changes(): |
| return None |
| else: |
| return get_git_commit('HEAD') |
| |
| def get_default_base_commit(current_commit): |
| if not current_commit: |
| if tree_changes(): |
| return get_git_commit('HEAD') |
| else: |
| return get_git_commit('HEAD~1') |
| else: |
| return get_git_commit('%s~1'%current_commit) |
| |
| def build_history(b_commit=None, c_commit=None): |
| if not GIT_ENABLED: |
| logger.info('Working on current tree, not git enabled.') |
| current_commit = None |
| base_commit = RELEASE_DATA |
| else: |
| if not c_commit: |
| current_commit = get_default_current_commit() |
| else: |
| current_commit = get_git_commit(c_commit) |
| |
| if not b_commit: |
| base_commit = get_default_base_commit(current_commit) |
| else: |
| base_commit = get_git_commit(b_commit) |
| |
| if not base_commit: |
| logger.error("Cannot resolve base commit") |
| return |
| |
| logger.info("Base: %s" % base_commit) |
| logger.info("Current: %s" % (current_commit if current_commit else |
| 'working space')) |
| |
| current_results = get_footprint_results(current_commit) |
| base_results = get_footprint_results(base_commit) |
| deltas = compare_results(base_results, current_results) |
| print_deltas(deltas) |
| |
| def compare_results(base_results, current_results): |
| interesting_metrics = [("ram_size", int), |
| ("rom_size", int)] |
| results = {} |
| metrics = {} |
| |
| for type, data in {'base': base_results, 'current': current_results}.items(): |
| metrics[type] = {} |
| for row in data: |
| d = {} |
| for m, mtype in interesting_metrics: |
| if row[m]: |
| d[m] = mtype(row[m]) |
| if not row["test"] in metrics[type]: |
| metrics[type][row["test"]] = {} |
| metrics[type][row["test"]][row["platform"]] = d |
| |
| for test, platforms in metrics['current'].items(): |
| if not test in metrics['base']: |
| continue |
| tests = {} |
| |
| for platform, test_data in platforms.items(): |
| if not platform in metrics['base'][test]: |
| continue |
| golden_metric = metrics['base'][test][platform] |
| tmp = {} |
| for metric, _ in interesting_metrics: |
| if metric not in golden_metric or metric not in test_data: |
| continue |
| if test_data[metric] == "": |
| continue |
| delta = test_data[metric] - golden_metric[metric] |
| if delta == 0: |
| continue |
| tmp[metric] = { |
| 'delta': delta, |
| 'current': test_data[metric], |
| } |
| |
| if tmp: |
| tests[platform] = tmp |
| |
| if tests: |
| results[test] = tests |
| |
| return results |
| |
| def print_deltas(deltas): |
| error_count = 0 |
| for test in sorted(deltas): |
| print("\n{:<25}".format(test)) |
| for platform, data in deltas[test].items(): |
| print(" {:<25}".format(platform)) |
| for metric, value in data.items(): |
| percentage = (float(value['delta']) / float(value['current'] - |
| value['delta'])) |
| print(" {} ({:+.2%}) {:+6} current size {:>7} bytes".format( |
| "RAM" if metric == "ram_size" else "ROM", percentage, |
| value['delta'], value['current'])) |
| error_count = error_count + 1 |
| if error_count == 0: |
| print("There are no changes in RAM neither in ROM of footprint apps.") |
| return error_count |
| |
| def main(): |
| args = parse_args() |
| build_history(args.base_commit, args.commit) |
| |
| if __name__ == "__main__": |
| init_logs() |
| is_git_enabled() |
| main() |