1#!/usr/bin/env python3
2"""
3This script parses benchmark log files generated by the lowlevel-blt-bench benchmark and
4compares the results. It can also export the parsed results to a CSV file.
5
6Usage:
7    python3 lowlevel_blt_bench_compare <log_file> [<log_file2>]
8        [--export-csv <csv_file>] [--quiet] [--color] [--color-breaks <breaks>]
9        [--disable-columns <columns>]
10
11Author: Marek Pikuła <m.pikula@partner.samsung.com>
12"""
13
14import argparse
15import re
16
17import pandas as pd
18from colorama import Fore, Style
19from tabulate import tabulate
20
21SPEEDUP_COLORS = [
22    Fore.RED,
23    Fore.LIGHTRED_EX,
24    Fore.YELLOW,
25    Fore.LIGHTYELLOW_EX,
26    Fore.LIGHTGREEN_EX,
27    Fore.GREEN,
28    Fore.BLUE,
29]
30
31
32def colorize_speedup(value: float, breaks: list[float]) -> str:
33    """Colorize the speedup value depending on the range of its value."""
34    color = SPEEDUP_COLORS[-1]
35    for c, b in zip(SPEEDUP_COLORS, breaks):
36        if value <= b:
37            color = c
38            break
39
40    return f"{color}{value}{Style.RESET_ALL}"
41
42
43def print_df(df: pd.DataFrame, color: bool, breaks: list[float], drop_cols: list[str]):
44    """Print the DataFrame with colorized speedup values."""
45    df.loc["Average"] = df.mean(axis=0)
46    df.drop(columns=drop_cols, errors="ignore", inplace=True)
47
48    table = tabulate(
49        df.map(colorize_speedup, breaks=breaks) if color else df,
50        floatfmt="2.3f",
51        headers="keys",
52        showindex=True,
53    )
54
55    # Print the table
56    print(table)
57
58
59def parse_benchmark_log(log_file: str):
60    """Parse a benchmark log file and return a DataFrame with the results."""
61
62    # Regular expression to match benchmark lines.
63    benchmark_regex = re.compile(
64        r"^\s*(\S+)\s*=\s*L1:\s*([\d.]+)\s*L2:\s*([\d.]+)\s*M:\s*([\d.]+).*"
65        r"HT:\s*([\d.]+)\s*VT:\s*([\d.]+)\s*R:\s*([\d.]+)\s*RT:\s*([\d.]+)\s*"
66        r"\(\s*([\d.]+).*\)"
67    )
68
69    # Read the log file and parse benchmark results using list comprehension.
70    with open(log_file, "r", encoding="utf-8") as file:
71        parsed_lines = tuple(
72            (
73                match.group(1),
74                map(float, match.groups()[1:]),
75            )
76            for line in file
77            if (match := benchmark_regex.match(line))
78        )
79
80        # Unpack parsed lines into functions and metrics.
81        functions, metrics = zip(*parsed_lines) if parsed_lines else ([], [])
82
83        # Create a DataFrame from the parsed data.
84        out = pd.DataFrame(
85            metrics,
86            index=functions,
87            columns=("L1", "L2", "M", "HT", "VT", "R", "RT", "Kops/s"),
88        )
89        out["Avg"] = out.mean(axis=1)
90        return out
91
92    return pd.DataFrame()
93
94
95if __name__ == "__main__":
96    # Set up argument parser.
97    parser = argparse.ArgumentParser(
98        description="Parse and compare lowlevel-blt-bench benchmark results.",
99    )
100    parser.add_argument(
101        "log_file",
102        help="Path to the first benchmark log file.",
103    )
104    parser.add_argument(
105        "log_file2",
106        nargs="?",
107        help="Path to the second benchmark log file (optional).",
108    )
109    parser.add_argument(
110        "--export-csv",
111        "-e",
112        metavar="CSV_FILE",
113        help="Export the parsed results to a CSV file.",
114    )
115    parser.add_argument(
116        "--quiet",
117        "-q",
118        action="store_true",
119        help="Don't print results (useful with --export-csv).",
120    )
121    parser.add_argument(
122        "--color",
123        "-c",
124        action="store_true",
125        help="Print table in color.",
126    )
127    parser.add_argument(
128        "--disable-columns",
129        "-d",
130        metavar="COLUMNS",
131        help="Comma-separated list of columns to disable (e.g., 'L1,L2,M').",
132    )
133    parser.add_argument(
134        "--color-breaks",
135        "-b",
136        metavar="BREAKS",
137        default="0.8,0.9,1.1,1.5,3.0,5.0",
138        help="Speedup values for color breaks (up to 6).",
139    )
140    args = parser.parse_args()
141
142    # Don't truncate DataFrame output.
143    pd.set_option("display.max_rows", None)
144
145    # Parse list arguments.
146    disabled_columns: list[str] = (
147        args.disable_columns.split(",") if args.disable_columns else []
148    )
149    color_breaks: list[float] = list(map(float, args.color_breaks.split(",")))
150
151    # Parse the first log file.
152    df1 = parse_benchmark_log(args.log_file)
153    to_export = df1
154
155    if args.log_file2:
156        # Parse the second log file and calculate speedup
157        df2 = parse_benchmark_log(args.log_file2)
158
159        # Align the two DataFrames based on their indices
160        df1, df2 = df1.align(df2, join="inner")
161
162        speedup = (df2 / df1) * 100000 // 100 / 1000
163
164        if not args.quiet:
165            print(f'Speedup between "{args.log_file}" and "{args.log_file2}":\n')
166            print_df(speedup, args.color, color_breaks, disabled_columns)
167
168        to_export = speedup
169    elif not args.quiet:
170        # Print the parsed DataFrame
171        print_df(df1, args.color, color_breaks, disabled_columns)
172
173    # Export parsed results to CSV if requested
174    if args.export_csv:
175        to_export.to_csv(args.export_csv)
176        if not args.quiet:
177            print(f"Parsed results exported to {args.export_csv}")
178