diff --git a/scripts/diff_complexity.py b/scripts/diff_complexity.py new file mode 100755 index 0000000000000000000000000000000000000000..1193b5b1c4b5b29a4d7c31104f7dc1e8288e4a4f --- /dev/null +++ b/scripts/diff_complexity.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +""" +(C) 2022-2024 IVAS codec Public Collaboration with portions copyright Dolby International AB, Ericsson AB, +Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V., Huawei Technologies Co. LTD., +Koninklijke Philips N.V., Nippon Telegraph and Telephone Corporation, Nokia Technologies Oy, Orange, +Panasonic Holdings Corporation, Qualcomm Technologies, Inc., VoiceAge Corporation, and other +contributors to this repository. All Rights Reserved. + +This software is protected by copyright law and by international treaties. +The IVAS codec Public Collaboration consisting of Dolby International AB, Ericsson AB, +Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V., Huawei Technologies Co. LTD., +Koninklijke Philips N.V., Nippon Telegraph and Telephone Corporation, Nokia Technologies Oy, Orange, +Panasonic Holdings Corporation, Qualcomm Technologies, Inc., VoiceAge Corporation, and other +contributors to this repository retain full ownership rights in their respective contributions in +the software. This notice grants no license of any kind, including but not limited to patent +license, nor is any license granted by implication, estoppel or otherwise. + +Contributors are required to enter into the IVAS codec Public Collaboration agreement before making +contributions. + +This software is provided "AS IS", without any express or implied warranties. The software is in the +development stage. It is intended exclusively for experts who have experience with such software and +solely for the purpose of inspection. All implied warranties of non-infringement, merchantability +and fitness for a particular purpose are hereby disclaimed and excluded. + +Any dispute, controversy or claim arising under or in relation to providing this software shall be +submitted to and settled by the final, binding jurisdiction of the courts of Munich, Germany in +accordance with the laws of the Federal Republic of Germany excluding its conflict of law rules and +the United Nations Convention on Contracts on the International Sales of Goods. +""" + +""" + Script to diff IVAS logs produced by WMC tool instrumented binaries + Recommended script usage is adding to path via symlink to somewhere in your $PATH, e.g.: + + ln -s diff_complexity.py ~/bin/cdiff + + This allows: + cdiff +""" +import argparse +import re +from io import StringIO +from shutil import get_terminal_size + +import pandas as pd + +REGEX_WMOPS_TABLE = r"\s?\w+(\s+\w+\.\w+)(\s+\w+\.\w+){3,6}" +REGEX_ROM = ( + r"((\w+\s+\w+)|(\w+\s+\w+\s+)\(.+\))\s?size\s+\(.+\/(\w+)\/.+\)\:\s(\d+)\s+\w+" +) +REGEX_MAX_MEM = r"(Maximum\s+.+)\s+size\:\s+(\d+)" + +PD_STRING_KWARGS = { + "index": False, + "justify": "center", + "max_colwidth": 30, +} +NOCOLOUR = "\x1b[0m" +RED = "\x1b[31m" +GREEN = "\x1b[32m" +BLUE = "\x1b[34m" + + +def log2df(log_file): + """ + Parse a WMC tool logfile to a pandas dataframe + """ + with open(log_file, "r") as log: + logfile = "".join(line for line in log) + + wmops = [ + re.sub(r"\s+", ",", w.group().strip()) + for w in re.finditer(REGEX_WMOPS_TABLE, logfile) + ] + memory = [m.expand(r"\1 (\4), \5") for m in re.finditer(REGEX_ROM, logfile)] + memory.extend([m.expand(r"\1, \2") for m in re.finditer(REGEX_MAX_MEM, logfile)]) + + if not wmops or not memory: + raise ValueError(f"Error parsing {log_file}!") + + wmops = pd.read_csv( + StringIO("\n".join(wmops)), + header=None, + names=[ + "Routine", + "Calls", + "WMOPs min", + "WMOPs max", + "WMOPs avg", + "WMOPs(cum) min", + "WMOPs(cum) max", + "WMOPs(cum) avg", + ], + ) + memory = pd.read_csv( + StringIO("\n".join(memory)), header=None, names=["Type", "Words"] + ) + return wmops, memory + + +def main(bsl, cut, out_file, quiet=False, verbose=False): + if not quiet: + print(GREEN + f"Baseline conditon: {bsl}" + NOCOLOUR) + print(RED + f"Condition under test: {cut}" + NOCOLOUR) + + bsl_wmops, bsl_mem = log2df(bsl) + cut_wmops, cut_mem = log2df(cut) + + if verbose: + PD_STRING_KWARGS["line_width"] = get_terminal_size()[0] + # outer merge on routines, only identical rows are tagged "BOTH" + merge = ( + pd.merge( + cut_wmops.set_index("Routine").drop("total").reset_index(), + bsl_wmops.set_index("Routine").drop("total").reset_index(), + how="outer", + indicator="Source", + ) + .sort_values(["Routine", "Source"], ascending=[True, False]) + .set_index("Source") + ) + merge.index = merge.index.rename_categories( + { + "left_only": RED + "CUT", + "right_only": GREEN + "BSL", + "both": BLUE + "BOTH", + } + ) + + unique = ( + merge.drop(BLUE + "BOTH", errors="ignore") + .reset_index() + .sort_values(["Routine", "Source"], ascending=[True, False]) + ) + common = ( + merge.drop(GREEN + "BSL", errors="ignore") + .drop(RED + "CUT", errors="ignore") + .reset_index() + .sort_values("Routine", ascending=False) + ) + + if not unique.empty: + print( + "Complexity difference of routines".center( + PD_STRING_KWARGS["line_width"], "-" + ) + ) + print(unique.to_string(**PD_STRING_KWARGS) + NOCOLOUR) + + if not common.empty: + print( + "Routines with no differences".center( + PD_STRING_KWARGS["line_width"], "-" + ) + ) + print(common.to_string(**PD_STRING_KWARGS) + NOCOLOUR) + else: + print( + "No differences in complexity of routines".center( + PD_STRING_KWARGS["line_width"], "-" + ) + ) + print(merge.to_string(**PD_STRING_KWARGS)) + SEPARATOR = "_" * PD_STRING_KWARGS["line_width"] + print(NOCOLOUR + SEPARATOR) + + table_wmops = pd.concat( + [ + bsl_wmops.iloc[-1][2:5], + cut_wmops.iloc[-1][2:5], + cut_wmops.iloc[-1][2:5] - bsl_wmops.iloc[-1][2:5], + ], + axis=1, + ) + table_wmops.columns = ["BSL", "CUT", "CUT - BSL"] + + table_mem = pd.concat( + [ + bsl_mem.iloc[:, 1], + cut_mem.iloc[:, 1], + cut_mem.iloc[:, 1] - bsl_mem.iloc[:, 1], + ], + axis=1, + ) + table_mem.set_index(bsl_mem.iloc[:, 0], inplace=True) + table_mem.columns = ["BSL", "CUT", "CUT - BSL"] + + table = pd.concat([table_wmops, table_mem]) + + def fmt_diff(x): + if isinstance(x, int): + fmt = "{}" + else: + fmt = "{:.3f}" + + if x > 0: + return RED + fmt.format(x) + NOCOLOUR + if x < 0: + return GREEN + fmt.format(x) + NOCOLOUR + else: + return BLUE + fmt.format(x) + NOCOLOUR + + table["CUT - BSL"] = table["CUT - BSL"].apply(fmt_diff) + + if not quiet: + print() + print(table.to_string(justify="left")) + + if out_file: + table.to_csv(out_file) + elif not quiet: + print("\nNo output file specified - console output only!") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="WMC TOOL instrumented log file diff tool" + ) + + parser.add_argument( + "bsl", + type=str, + help="input logfile for baseline condition", + ) + + parser.add_argument( + "cut", + type=str, + help="input logfile for condition under test", + ) + + parser.add_argument( + "-o", + "--outfile", + required=False, + type=str, + help="output csv table", + ) + + parser.add_argument( + "-q", + "--quiet", + required=False, + action="store_true", + help="no console output", + default=False, + ) + + parser.add_argument( + "-v", + "--verbose", + required=False, + action="store_true", + help="print detailed info about routines", + default=False, + ) + + args = parser.parse_args() + + if args.verbose and args.quiet: + print("Both verbose and quiet options specified, defaulting to verbose") + args.quiet = False + + main(args.bsl, args.cut, args.outfile, args.quiet, args.verbose)