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