From f7aa7b9e09f81970cae710fdd4a6f12aae375d8b Mon Sep 17 00:00:00 2001 From: Daniel Friesel Date: Fri, 8 Oct 2021 14:12:19 +0200 Subject: add basic analyze-kconfig script --- bin/analyze-kconfig.py | 224 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/loader/__init__.py | 1 + lib/loader/kconfig.py | 112 +++++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100755 bin/analyze-kconfig.py create mode 100644 lib/loader/kconfig.py diff --git a/bin/analyze-kconfig.py b/bin/analyze-kconfig.py new file mode 100755 index 0000000..da97c58 --- /dev/null +++ b/bin/analyze-kconfig.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 + +"""analyze-kconfig - Generate a model for KConfig selections + +analyze-kconfig builds a model determining system attributes +(e.g. ROM or RAM usage) based on KConfig configuration variables. +Only boolean variables are supported at the moment. +""" + +import argparse +import json +import kconfiglib +import logging +import os + +import numpy as np + +import dfatool.utils +from dfatool.loader import KConfigAttributes +from dfatool.model import AnalyticModel + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__ + ) + parser.add_argument( + "--show-failing-symbols", + action="store_true", + help="Show Kconfig symbols related to build failures. Must be used with an experiment result directory.", + ) + parser.add_argument( + "--show-nop-symbols", + action="store_true", + help="Show Kconfig symbols which are only present in a single configuration. Must be used with an experiment result directory.", + ) + parser.add_argument( + "--force-tree", + action="store_true", + help="Build decision tree without checking for analytic functions first. Use this for large kconfig files.", + ) + parser.add_argument( + "--export-tree", + type=str, + help="Export kconfig-webconf model to file", + metavar="FILE", + ) + parser.add_argument( + "--config", + type=str, + help="Show model results for symbols in .config file", + metavar="FILE", + ) + parser.add_argument( + "--log-level", + default=logging.INFO, + type=lambda level: getattr(logging, level.upper()), + help="Set log level", + ) + parser.add_argument( + "--info", action="store_true", help="Show Kconfig and benchmark information" + ) + parser.add_argument( + "--sample-size", + type=int, + help="Restrict model generation to N random samples", + metavar="N", + ) + parser.add_argument("kconfig_path", type=str, help="Path to Kconfig file") + parser.add_argument( + "model", + type=str, + help="Path to experiment results directory or model.json file", + ) + + args = parser.parse_args() + + if isinstance(args.log_level, int): + logging.basicConfig(level=args.log_level) + else: + print(f"Invalid log level. Setting log level to INFO.", file=sys.stderr) + + if os.path.isdir(args.model): + attributes = KConfigAttributes(args.kconfig_path, args.model) + + if args.show_failing_symbols: + show_failing_symbols(attributes) + if args.show_nop_symbols: + show_nop_symbols(attributes) + + observations = list() + + for param, attr in attributes.data: + for key, value in attr.items(): + observations.append( + { + "name": key, + "param": param, + "attribute": value, + } + ) + + if args.sample_size: + shuffled_data_indices = np.random.permutation( + np.arange(len(attributes.data)) + ) + sample_indices = shuffled_data_indices[: args.sample_size] + raise RuntimeError("Not Implemented") + + by_name, parameter_names = dfatool.utils.observations_to_by_name(observations) + + model = AnalyticModel( + by_name, parameter_names, compute_stats=not args.force_tree + ) + + if args.force_tree: + for name in model.names: + for attr in model.by_name[name]["attributes"]: + # TODO specify correct threshold + model.build_dtree(name, attr, 20) + + else: + raise NotImplementedError() + + if args.info: + print("TODO") + + if args.export_tree: + with open(args.export_tree, "w") as f: + json.dump(model.to_json(), f, sort_keys=True, cls=dfatool.utils.NpEncoder) + + if args.config: + kconf = kconfiglib.Kconfig(args.kconfig_path) + kconf.load_config(args.config) + print(f"Model result for .config: {model.value_for_config(kconf)}") + + for symbol in model.symbols: + kconf2 = kconfiglib.Kconfig(args.kconfig_path) + kconf2.load_config(args.config) + kconf_sym = kconf2.syms[symbol] + if kconf_sym.tri_value == 0 and 2 in kconf_sym.assignable: + kconf_sym.set_value(2) + elif kconf_sym.tri_value == 2 and 0 in kconf_sym.assignable: + kconf_sym.set_value(0) + else: + continue + + # specific to multipass: + # Do not suggest changes which affect the application + skip = False + num_changes = 0 + changed_symbols = list() + for i, csymbol in enumerate(model.symbols): + if kconf.syms[csymbol].tri_value != kconf2.syms[csymbol].tri_value: + num_changes += 1 + changed_symbols.append(csymbol) + if ( + csymbol.startswith("app_") + and kconf.syms[csymbol].tri_value + != kconf2.syms[csymbol].tri_value + ): + skip = True + break + if skip: + continue + + try: + model_diff = model.value_for_config(kconf2) - model.value_for_config( + kconf + ) + if kconf_sym.choice: + print( + f"Setting {kconf_sym.choice.name} to {kconf_sym.name} changes {num_changes:2d} symbols, model change: {model_diff:+5.0f}" + ) + else: + print( + f"Setting {symbol} to {kconf_sym.str_value} changes {num_changes:2d} symbols, model change: {model_diff:+5.0f}" + ) + except TypeError: + if kconf_sym.choice: + print( + f"Setting {kconf_sym.choice.name} to {kconf_sym.name} changes {num_changes:2d} symbols, model is undefined" + ) + else: + print( + f"Setting {symbol} to {kconf_sym.str_value} changes {num_changes:2d} symbols, model is undefined" + ) + for changed_symbol in changed_symbols: + print( + f" {changed_symbol:30s} -> {kconf2.syms[changed_symbol].str_value}" + ) + + +def show_failing_symbols(data): + for symbol in data.param_names: + unique_values = list(set(map(lambda p: p[symbol], data.failures))) + for value in unique_values: + fail_count = len(list(filter(lambda p: p[symbol] == value, data.failures))) + success_count = len( + list(filter(lambda p: p[0][symbol] == value, data.data)) + ) + if success_count == 0 and fail_count > 0: + print( + f"Setting {symbol} to '{value}' reliably causes the build to fail (count = {fail_count})" + ) + + +def show_nop_symbols(data): + for symbol in data.symbol_names: + true_count = len( + list(filter(lambda config: config[symbol] == True, data.failures)) + ) + len(list(filter(lambda config: config[0][symbol] == True, data.data))) + false_count = len( + list(filter(lambda config: config[symbol] == False, data.failures)) + ) + len(list(filter(lambda config: config[0][symbol] == False, data.data))) + if false_count == 0: + print(f"Symbol {symbol} is never n") + if true_count == 0: + print(f"Symbol {symbol} is never y") + pass + + +if __name__ == "__main__": + main() diff --git a/lib/loader/__init__.py b/lib/loader/__init__.py index 141c7ca..8f730bb 100644 --- a/lib/loader/__init__.py +++ b/lib/loader/__init__.py @@ -19,6 +19,7 @@ from .energytrace import ( EnergyTraceWithLogicAnalyzer, EnergyTraceWithTimer, ) +from .kconfig import KConfigAttributes from .keysight import DLog, KeysightCSV from .mimosa import MIMOSA diff --git a/lib/loader/kconfig.py b/lib/loader/kconfig.py new file mode 100644 index 0000000..1dbebc8 --- /dev/null +++ b/lib/loader/kconfig.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +import kconfiglib +from frozendict import frozendict +import json +import os +import subprocess + + +class KConfigAttributes: + def __init__(self, kconfig_path, datadir): + experiments = list() + failed_experiments = list() + for direntry in os.listdir(datadir): + config_path = f"{datadir}/{direntry}/.config" + attr_path = f"{datadir}/{direntry}/attributes.json" + metadata_path = f"{datadir}/{direntry}/metadata" + if os.path.exists(attr_path): + experiments.append((config_path, attr_path)) + elif os.path.exists(config_path): + failed_experiments.append(config_path) + + kconf = kconfiglib.Kconfig(kconfig_path) + self.kconf = kconf + + self.kconfig_hash = self.file_hash(kconfig_path) + self.kconfig_dir = "unknown" + if "/" in kconfig_path: + self.kconfig_dir = kconfig_path.split("/")[-2] + + self.symbol_names = sorted( + map( + lambda sym: sym.name, + filter( + lambda sym: kconfiglib.TYPE_TO_STR[sym.type] + in ("bool", "tristate", "int", "string", "hex"), + kconf.syms.values(), + ), + ) + ) + + self.choice_names = sorted( + map(lambda choice: choice.name or choice.name_and_loc, kconf.choices) + ) + + self.choice = dict() + self.choice_symbol_names = list() + for choice in kconf.choices: + self.choice[choice.name or choice.name_and_loc] = choice + self.choice_symbol_names.extend(map(lambda sym: sym.name, choice.syms)) + + self.symbol = dict() + for symbol_name in self.symbol_names: + self.symbol[symbol_name] = kconf.syms[symbol_name] + + if int(os.getenv("DFATOOL_KCONF_WITH_CHOICE_NODES", 1)): + for sym_name in self.choice_symbol_names: + self.symbol_names.remove(sym_name) + self.param_names = self.symbol_names + self.choice_names + else: + self.param_names = self.symbol_names + + self.data = list() + self.configs = list() + self.failures = list() + + for config_path, attr_path in experiments: + self.configs.append(config_path) + kconf.load_config(config_path) + with open(attr_path, "r") as f: + attr = json.load(f) + + param = self._conf_to_param() + self.data.append((frozendict(param), attr)) + + for config_path in failed_experiments: + kconf.load_config(config_path) + param = self._conf_to_param() + self.failures.append(frozendict(param)) + + def _conf_to_param(self): + param = dict() + for sym_name in self.symbol_names: + sym = self.kconf.syms[sym_name] + if not sym.visibility and sym.str_value == "": + param[sym_name] = None + elif kconfiglib.TYPE_TO_STR[sym.type] in ("int", "hex"): + try: + param[sym_name] = int(sym.str_value, base=0) + except ValueError: + print( + f"Warning: Illegal value for {sym.__repr__()}, defaulting to None" + ) + param[sym_name] = None + else: + param[sym_name] = sym.str_value + for choice in self.choice_names: + if self.choice[choice].selection is None: + param[choice] = None + else: + param[choice] = self.choice[choice].selection.name + return param + + def file_hash(self, config_file): + status = subprocess.run( + ["sha256sum", config_file], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + sha256sum = status.stdout.split()[0] + return sha256sum -- cgit v1.2.3