|  | """ | 
|  | Static Analyzer qualification infrastructure. | 
|  |  | 
|  | This source file contains all the functionality related to benchmarking | 
|  | the analyzer on a set projects.  Right now, this includes measuring | 
|  | execution time and peak memory usage.  Benchmark runs analysis on every | 
|  | project multiple times to get a better picture about the distribution | 
|  | of measured values. | 
|  |  | 
|  | Additionally, this file includes a comparison routine for two benchmarking | 
|  | results that plots the result together on one chart. | 
|  | """ | 
|  |  | 
|  | import SATestUtils as utils | 
|  | from SATestBuild import ProjectTester, stdout, TestInfo | 
|  | from ProjectMap import ProjectInfo | 
|  |  | 
|  | import pandas as pd | 
|  | from typing import List, Tuple | 
|  |  | 
|  |  | 
|  | INDEX_COLUMN = "index" | 
|  |  | 
|  |  | 
|  | def _save(data: pd.DataFrame, file_path: str): | 
|  | data.to_csv(file_path, index_label=INDEX_COLUMN) | 
|  |  | 
|  |  | 
|  | def _load(file_path: str) -> pd.DataFrame: | 
|  | return pd.read_csv(file_path, index_col=INDEX_COLUMN) | 
|  |  | 
|  |  | 
|  | class Benchmark: | 
|  | """ | 
|  | Becnhmark class encapsulates one functionality: it runs the analysis | 
|  | multiple times for the given set of projects and stores results in the | 
|  | specified file. | 
|  | """ | 
|  |  | 
|  | def __init__(self, projects: List[ProjectInfo], iterations: int, output_path: str): | 
|  | self.projects = projects | 
|  | self.iterations = iterations | 
|  | self.out = output_path | 
|  |  | 
|  | def run(self): | 
|  | results = [self._benchmark_project(project) for project in self.projects] | 
|  |  | 
|  | data = pd.concat(results, ignore_index=True) | 
|  | _save(data, self.out) | 
|  |  | 
|  | def _benchmark_project(self, project: ProjectInfo) -> pd.DataFrame: | 
|  | if not project.enabled: | 
|  | stdout(f" \n\n--- Skipping disabled project {project.name}\n") | 
|  | return | 
|  |  | 
|  | stdout(f" \n\n--- Benchmarking project {project.name}\n") | 
|  |  | 
|  | test_info = TestInfo(project) | 
|  | tester = ProjectTester(test_info, silent=True) | 
|  | project_dir = tester.get_project_dir() | 
|  | output_dir = tester.get_output_dir() | 
|  |  | 
|  | raw_data = [] | 
|  |  | 
|  | for i in range(self.iterations): | 
|  | stdout(f"Iteration #{i + 1}") | 
|  | time, mem = tester.build(project_dir, output_dir) | 
|  | raw_data.append( | 
|  | {"time": time, "memory": mem, "iteration": i, "project": project.name} | 
|  | ) | 
|  | stdout( | 
|  | f"time: {utils.time_to_str(time)}, " | 
|  | f"peak memory: {utils.memory_to_str(mem)}" | 
|  | ) | 
|  |  | 
|  | return pd.DataFrame(raw_data) | 
|  |  | 
|  |  | 
|  | def compare(old_path: str, new_path: str, plot_file: str): | 
|  | """ | 
|  | Compare two benchmarking results stored as .csv files | 
|  | and produce a plot in the specified file. | 
|  | """ | 
|  | old = _load(old_path) | 
|  | new = _load(new_path) | 
|  |  | 
|  | old_projects = set(old["project"]) | 
|  | new_projects = set(new["project"]) | 
|  | common_projects = old_projects & new_projects | 
|  |  | 
|  | # Leave only rows for projects common to both dataframes. | 
|  | old = old[old["project"].isin(common_projects)] | 
|  | new = new[new["project"].isin(common_projects)] | 
|  |  | 
|  | old, new = _normalize(old, new) | 
|  |  | 
|  | # Seaborn prefers all the data to be in one dataframe. | 
|  | old["kind"] = "old" | 
|  | new["kind"] = "new" | 
|  | data = pd.concat([old, new], ignore_index=True) | 
|  |  | 
|  | # TODO: compare data in old and new dataframes using statistical tests | 
|  | #       to check if they belong to the same distribution | 
|  | _plot(data, plot_file) | 
|  |  | 
|  |  | 
|  | def _normalize( | 
|  | old: pd.DataFrame, new: pd.DataFrame | 
|  | ) -> Tuple[pd.DataFrame, pd.DataFrame]: | 
|  | # This creates a dataframe with all numerical data averaged. | 
|  | means = old.groupby("project").mean() | 
|  | return _normalize_impl(old, means), _normalize_impl(new, means) | 
|  |  | 
|  |  | 
|  | def _normalize_impl(data: pd.DataFrame, means: pd.DataFrame): | 
|  | # Right now 'means' has one row corresponding to one project, | 
|  | # while 'data' has N rows for each project (one for each iteration). | 
|  | # | 
|  | # In order for us to work easier with this data, we duplicate | 
|  | # 'means' data to match the size of the 'data' dataframe. | 
|  | # | 
|  | # All the columns from 'data' will maintain their names, while | 
|  | # new columns coming from 'means' will have "_mean" suffix. | 
|  | joined_data = data.merge(means, on="project", suffixes=("", "_mean")) | 
|  | _normalize_key(joined_data, "time") | 
|  | _normalize_key(joined_data, "memory") | 
|  | return joined_data | 
|  |  | 
|  |  | 
|  | def _normalize_key(data: pd.DataFrame, key: str): | 
|  | norm_key = _normalized_name(key) | 
|  | mean_key = f"{key}_mean" | 
|  | data[norm_key] = data[key] / data[mean_key] | 
|  |  | 
|  |  | 
|  | def _normalized_name(name: str) -> str: | 
|  | return f"normalized {name}" | 
|  |  | 
|  |  | 
|  | def _plot(data: pd.DataFrame, plot_file: str): | 
|  | import matplotlib | 
|  | import seaborn as sns | 
|  | from matplotlib import pyplot as plt | 
|  |  | 
|  | sns.set_style("whitegrid") | 
|  | # We want to have time and memory charts one above the other. | 
|  | figure, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 6)) | 
|  |  | 
|  | def _subplot(key: str, ax: matplotlib.axes.Axes): | 
|  | sns.boxplot( | 
|  | x="project", | 
|  | y=_normalized_name(key), | 
|  | hue="kind", | 
|  | data=data, | 
|  | palette=sns.color_palette("BrBG", 2), | 
|  | ax=ax, | 
|  | ) | 
|  |  | 
|  | _subplot("time", ax1) | 
|  | # No need to have xlabels on both top and bottom charts. | 
|  | ax1.set_xlabel("") | 
|  |  | 
|  | _subplot("memory", ax2) | 
|  | # The legend on the top chart is enough. | 
|  | ax2.get_legend().remove() | 
|  |  | 
|  | figure.savefig(plot_file) |