summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbin/analyze-kconfig.py224
-rw-r--r--lib/loader/__init__.py1
-rw-r--r--lib/loader/kconfig.py112
3 files changed, 337 insertions, 0 deletions
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