xref: /openbsd-src/gnu/llvm/clang/utils/analyzer/SATestBenchmark.py (revision ec727ea710c91afd8ce4f788c5aaa8482b7b69b2)
1*ec727ea7Spatrick"""
2*ec727ea7SpatrickStatic Analyzer qualification infrastructure.
3*ec727ea7Spatrick
4*ec727ea7SpatrickThis source file contains all the functionality related to benchmarking
5*ec727ea7Spatrickthe analyzer on a set projects.  Right now, this includes measuring
6*ec727ea7Spatrickexecution time and peak memory usage.  Benchmark runs analysis on every
7*ec727ea7Spatrickproject multiple times to get a better picture about the distribution
8*ec727ea7Spatrickof measured values.
9*ec727ea7Spatrick
10*ec727ea7SpatrickAdditionally, this file includes a comparison routine for two benchmarking
11*ec727ea7Spatrickresults that plots the result together on one chart.
12*ec727ea7Spatrick"""
13*ec727ea7Spatrick
14*ec727ea7Spatrickimport SATestUtils as utils
15*ec727ea7Spatrickfrom SATestBuild import ProjectTester, stdout, TestInfo
16*ec727ea7Spatrickfrom ProjectMap import ProjectInfo
17*ec727ea7Spatrick
18*ec727ea7Spatrickimport pandas as pd
19*ec727ea7Spatrickfrom typing import List, Tuple
20*ec727ea7Spatrick
21*ec727ea7Spatrick
22*ec727ea7SpatrickINDEX_COLUMN = "index"
23*ec727ea7Spatrick
24*ec727ea7Spatrick
25*ec727ea7Spatrickdef _save(data: pd.DataFrame, file_path: str):
26*ec727ea7Spatrick    data.to_csv(file_path, index_label=INDEX_COLUMN)
27*ec727ea7Spatrick
28*ec727ea7Spatrick
29*ec727ea7Spatrickdef _load(file_path: str) -> pd.DataFrame:
30*ec727ea7Spatrick    return pd.read_csv(file_path, index_col=INDEX_COLUMN)
31*ec727ea7Spatrick
32*ec727ea7Spatrick
33*ec727ea7Spatrickclass Benchmark:
34*ec727ea7Spatrick    """
35*ec727ea7Spatrick    Becnhmark class encapsulates one functionality: it runs the analysis
36*ec727ea7Spatrick    multiple times for the given set of projects and stores results in the
37*ec727ea7Spatrick    specified file.
38*ec727ea7Spatrick    """
39*ec727ea7Spatrick    def __init__(self, projects: List[ProjectInfo], iterations: int,
40*ec727ea7Spatrick                 output_path: str):
41*ec727ea7Spatrick        self.projects = projects
42*ec727ea7Spatrick        self.iterations = iterations
43*ec727ea7Spatrick        self.out = output_path
44*ec727ea7Spatrick
45*ec727ea7Spatrick    def run(self):
46*ec727ea7Spatrick        results = [self._benchmark_project(project)
47*ec727ea7Spatrick                   for project in self.projects]
48*ec727ea7Spatrick
49*ec727ea7Spatrick        data = pd.concat(results, ignore_index=True)
50*ec727ea7Spatrick        _save(data, self.out)
51*ec727ea7Spatrick
52*ec727ea7Spatrick    def _benchmark_project(self, project: ProjectInfo) -> pd.DataFrame:
53*ec727ea7Spatrick        if not project.enabled:
54*ec727ea7Spatrick            stdout(f" \n\n--- Skipping disabled project {project.name}\n")
55*ec727ea7Spatrick            return
56*ec727ea7Spatrick
57*ec727ea7Spatrick        stdout(f" \n\n--- Benchmarking project {project.name}\n")
58*ec727ea7Spatrick
59*ec727ea7Spatrick        test_info = TestInfo(project)
60*ec727ea7Spatrick        tester = ProjectTester(test_info, silent=True)
61*ec727ea7Spatrick        project_dir = tester.get_project_dir()
62*ec727ea7Spatrick        output_dir = tester.get_output_dir()
63*ec727ea7Spatrick
64*ec727ea7Spatrick        raw_data = []
65*ec727ea7Spatrick
66*ec727ea7Spatrick        for i in range(self.iterations):
67*ec727ea7Spatrick            stdout(f"Iteration #{i + 1}")
68*ec727ea7Spatrick            time, mem = tester.build(project_dir, output_dir)
69*ec727ea7Spatrick            raw_data.append({"time": time, "memory": mem,
70*ec727ea7Spatrick                             "iteration": i, "project": project.name})
71*ec727ea7Spatrick            stdout(f"time: {utils.time_to_str(time)}, "
72*ec727ea7Spatrick                   f"peak memory: {utils.memory_to_str(mem)}")
73*ec727ea7Spatrick
74*ec727ea7Spatrick        return pd.DataFrame(raw_data)
75*ec727ea7Spatrick
76*ec727ea7Spatrick
77*ec727ea7Spatrickdef compare(old_path: str, new_path: str, plot_file: str):
78*ec727ea7Spatrick    """
79*ec727ea7Spatrick    Compare two benchmarking results stored as .csv files
80*ec727ea7Spatrick    and produce a plot in the specified file.
81*ec727ea7Spatrick    """
82*ec727ea7Spatrick    old = _load(old_path)
83*ec727ea7Spatrick    new = _load(new_path)
84*ec727ea7Spatrick
85*ec727ea7Spatrick    old_projects = set(old["project"])
86*ec727ea7Spatrick    new_projects = set(new["project"])
87*ec727ea7Spatrick    common_projects = old_projects & new_projects
88*ec727ea7Spatrick
89*ec727ea7Spatrick    # Leave only rows for projects common to both dataframes.
90*ec727ea7Spatrick    old = old[old["project"].isin(common_projects)]
91*ec727ea7Spatrick    new = new[new["project"].isin(common_projects)]
92*ec727ea7Spatrick
93*ec727ea7Spatrick    old, new = _normalize(old, new)
94*ec727ea7Spatrick
95*ec727ea7Spatrick    # Seaborn prefers all the data to be in one dataframe.
96*ec727ea7Spatrick    old["kind"] = "old"
97*ec727ea7Spatrick    new["kind"] = "new"
98*ec727ea7Spatrick    data = pd.concat([old, new], ignore_index=True)
99*ec727ea7Spatrick
100*ec727ea7Spatrick    # TODO: compare data in old and new dataframes using statistical tests
101*ec727ea7Spatrick    #       to check if they belong to the same distribution
102*ec727ea7Spatrick    _plot(data, plot_file)
103*ec727ea7Spatrick
104*ec727ea7Spatrick
105*ec727ea7Spatrickdef _normalize(old: pd.DataFrame,
106*ec727ea7Spatrick               new: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
107*ec727ea7Spatrick    # This creates a dataframe with all numerical data averaged.
108*ec727ea7Spatrick    means = old.groupby("project").mean()
109*ec727ea7Spatrick    return _normalize_impl(old, means), _normalize_impl(new, means)
110*ec727ea7Spatrick
111*ec727ea7Spatrick
112*ec727ea7Spatrickdef _normalize_impl(data: pd.DataFrame, means: pd.DataFrame):
113*ec727ea7Spatrick    # Right now 'means' has one row corresponding to one project,
114*ec727ea7Spatrick    # while 'data' has N rows for each project (one for each iteration).
115*ec727ea7Spatrick    #
116*ec727ea7Spatrick    # In order for us to work easier with this data, we duplicate
117*ec727ea7Spatrick    # 'means' data to match the size of the 'data' dataframe.
118*ec727ea7Spatrick    #
119*ec727ea7Spatrick    # All the columns from 'data' will maintain their names, while
120*ec727ea7Spatrick    # new columns coming from 'means' will have "_mean" suffix.
121*ec727ea7Spatrick    joined_data = data.merge(means, on="project", suffixes=("", "_mean"))
122*ec727ea7Spatrick    _normalize_key(joined_data, "time")
123*ec727ea7Spatrick    _normalize_key(joined_data, "memory")
124*ec727ea7Spatrick    return joined_data
125*ec727ea7Spatrick
126*ec727ea7Spatrick
127*ec727ea7Spatrickdef _normalize_key(data: pd.DataFrame, key: str):
128*ec727ea7Spatrick    norm_key = _normalized_name(key)
129*ec727ea7Spatrick    mean_key = f"{key}_mean"
130*ec727ea7Spatrick    data[norm_key] = data[key] / data[mean_key]
131*ec727ea7Spatrick
132*ec727ea7Spatrick
133*ec727ea7Spatrickdef _normalized_name(name: str) -> str:
134*ec727ea7Spatrick    return f"normalized {name}"
135*ec727ea7Spatrick
136*ec727ea7Spatrick
137*ec727ea7Spatrickdef _plot(data: pd.DataFrame, plot_file: str):
138*ec727ea7Spatrick    import matplotlib
139*ec727ea7Spatrick    import seaborn as sns
140*ec727ea7Spatrick    from matplotlib import pyplot as plt
141*ec727ea7Spatrick
142*ec727ea7Spatrick    sns.set_style("whitegrid")
143*ec727ea7Spatrick    # We want to have time and memory charts one above the other.
144*ec727ea7Spatrick    figure, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 6))
145*ec727ea7Spatrick
146*ec727ea7Spatrick    def _subplot(key: str, ax: matplotlib.axes.Axes):
147*ec727ea7Spatrick        sns.boxplot(x="project", y=_normalized_name(key), hue="kind",
148*ec727ea7Spatrick                    data=data, palette=sns.color_palette("BrBG", 2), ax=ax)
149*ec727ea7Spatrick
150*ec727ea7Spatrick    _subplot("time", ax1)
151*ec727ea7Spatrick    # No need to have xlabels on both top and bottom charts.
152*ec727ea7Spatrick    ax1.set_xlabel("")
153*ec727ea7Spatrick
154*ec727ea7Spatrick    _subplot("memory", ax2)
155*ec727ea7Spatrick    # The legend on the top chart is enough.
156*ec727ea7Spatrick    ax2.get_legend().remove()
157*ec727ea7Spatrick
158*ec727ea7Spatrick    figure.savefig(plot_file)
159