File size: 15,679 Bytes
ff388c9
 
 
 
173c265
ff388c9
 
 
 
 
 
 
 
dea020f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5ebc38
dea020f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
"""
Business Source License 1.1

Copyright (C) 2025 Raman Marozau, raman@worktif.com
Use of this software is governed by the Business Source License included in the LICENSE file and at www.mariadb.com/bsl11.

Change Date: Never
On the date above, in accordance with the Business Source License, use of this software will be governed by the open source license specified in the LICENSE file.
Additional Use Grant: Free for personal and non-commercial research use only.

SPDX-License-Identifier: BUSL-1.1
"""

import datetime
import os

import json
import warnings
from pathlib import Path
from typing import List

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from pandas.core.interchange.dataframe_protocol import DataFrame
from scipy.signal import savgol_filter


class BenchmarksAnalysis:
    """
    Class for managing the analysis of benchmarks.

    This class provides functionality to handle benchmark data including loading,
    processing into a DataFrame, and generating various visual plots for performance
    analysis. It ensures proper organization of benchmark files and provides specific
    plot types to assess performance metrics and acceleration factors across a range
    of input sizes.

    :ivar benchmarks_plot_dir: Directory for storing generated benchmark plots.
    :type benchmarks_plot_dir: pathlib.Path
    :ivar test_number: The integer representation of the test identifier.
    :type test_number: int
    """

    def __init__(self, test_id: str) -> None:
        self.benchmarks_plot_dir = self.benchmarks_setup()
        self.test_number = int(test_id)

    @staticmethod
    def benchmarks_setup() -> Path:
        """
        Creates and initializes a directory for storing benchmark plots.

        This static method generates a uniquely named directory based on the current
        date and time in the "purem_benchmarks" directory relative to the script's
        location. If the directory does not already exist, it will be created. The
        directory name includes a timestamp in the format "YYYY-MM-DD_HH_MM_SS__FFF".

        :return: A `Path` object pointing to the created directory.
        :rtype: Path
        """
        current_time = datetime.datetime.now()
        benchmarks_id: str = f"""purem_benchmarks_{current_time.strftime("%Y-%m-%d_%H_%M_%S__%f")[:-4]}"""
        current_dir: str = os.path.dirname(__file__)
        benchmarks_plot_dir: Path = Path(current_dir) / "purem_benchmarks" / benchmarks_id
        os.makedirs(benchmarks_plot_dir, exist_ok=True)

        return benchmarks_plot_dir

    @staticmethod
    def extract_size_from_filepath(filepath: str) -> int:
        """
        Extract the `size` value from the given file path.
        Assumes the size is encoded as the last part before the file extension.
        """
        split_filepath = filepath.split("_")
        return int(split_filepath[-1].split(".")[0])

    def run(self) -> None:
        """
        Executes the process of generating benchmarks and acceleration plots, as well as creating
        and saving Markdown-formatted data frames. This function orchestrates the pipeline by
        calling several helper methods to handle plotting and data frame construction.

        :raises AnyRelevantError: Description of why and when it might be raised.

        :return: None
        """
        benchmarks_df = self.benchmarks_data_frame()
        self.benchmarks_plot(benchmarks_df)
        self.acceleration_plot(benchmarks_df)

        markdown_df = self.markdown_data_frame()
        self.markdown_save(markdown_df)

    def load_benchmarks_files(self) -> List[str]:
        """
        Loads benchmark files matching a specific test number from a designated directory.
        The function identifies files in the benchmarks directory that match a given test number,
        extracts a unique timestamp from the matching files, and retrieves all files that share
        the same timestamp. This ensures that all related benchmark files for a specific test are
        collected efficiently.

        :param test_number: An integer representing the test number used to filter and locate
            benchmark files. The test number will be zero-padded to ensure a four-digit format.
        :return: A list of file paths for all benchmark files associated with the specified test
            number and same timestamp.
        :rtype: List[str]
        :raises FileNotFoundError: If no benchmark files matching the specified test number
            are found in the benchmarks directory.
        """
        benchmarks_dir = ".benchmarks/Darwin-CPython-3.11-64bit"
        search_pattern = str(self.test_number).zfill(4)

        matched_files = [f for f in os.listdir(benchmarks_dir) if f.startswith(search_pattern) and f.endswith(".json")]

        if not matched_files:
            raise FileNotFoundError(f"No benchmark files found starting with {search_pattern} in {benchmarks_dir}")

        file_stamp = matched_files[0].split("_")[2]
        return [
            os.path.join(benchmarks_dir, f)
            for f in os.listdir(benchmarks_dir)
            if f.split("_")[2] == file_stamp and f.endswith(".json")
        ]

    def benchmarks_data_frame(self) -> pd.DataFrame:
        """
        Generates a Pandas DataFrame containing benchmark data extracted
        from all related files. The method processes multiple files, loads
        their content, extracts benchmark statistics, and organizes them
        into a structured tabular format.

        :param self: The instance of the class containing this method.

        :returns: A pandas DataFrame where each row represents a single
            benchmark entry. The columns include 'size', 'func', 'min',
            'max', 'mean', 'stddev', and 'ops', corresponding to the
            benchmark statistics and other metadata.
        :rtype: pd.DataFrame
        """
        files = self.load_benchmarks_files()

        records = []

        for filepath in files:
            with open(filepath, "r") as f:
                data = json.load(f)

            filepath_split = filepath.split("_")

            for entry in data["benchmarks"]:
                size = self.extract_size_from_filepath(filepath)
                func = entry["params"]["func_name"]
                stats = entry["stats"]
                records.append({
                    "size": size,
                    "func": func,
                    "min": stats["min"],
                    "max": stats["max"],
                    "mean": stats["mean"],
                    "stddev": stats["stddev"],
                    "ops": stats["ops"]
                })

        return pd.DataFrame(records)

    def benchmarks_plot(self, df: DataFrame) -> None:
        """
        Generates and saves benchmarking plots for various performance metrics.

        This function iterates over a defined list of metrics such as "min", "max", "mean",
        "stddev", and "ops", and generates two types of plots for each metric:
        one for the full range of input sizes and another for large input sizes only.
        Each plot visualizes the relationship between input sizes and metric values for all
        functions provided in the DataFrame. The x-axis is set to log scale for both
        plots. The y-axis is also set to log scale for all metrics except "stddev".
        The generated plots are saved as PNG files in a directory defined by
        `self.benchmarks_plot_dir`.

        :param df: A DataFrame containing benchmarking data. The DataFrame must have the
            following columns:
            - "func": Names of the functions being benchmarked.
            - "size": Input sizes corresponding to the benchmarks.
            - Metric columns (e.g., "min", "max", "mean", "stddev", "ops"): Values of the
              respective performance metrics.
        """
        metrics = ["min", "max", "mean", "stddev", "ops"]

        for metric in metrics:
            plt.figure(figsize=(10, 6))
            for func_name in df["func"].unique():
                data_by_func = df[df["func"] == func_name].sort_values("size")
                plt.plot(data_by_func["size"], data_by_func[metric], label=func_name, marker='o')
            plt.title(f"{metric.upper()} vs Size (Full Range)")
            plt.xlabel("Input Size")
            plt.ylabel(metric)
            plt.legend()
            plt.grid(True)
            plt.xscale("log")
            if metric != "stddev":
                plt.yscale("log")
            plt.tight_layout()
            plt.savefig(f"{self.benchmarks_plot_dir}/benchmark_{metric}_full.png")
            plt.close()

            plt.figure(figsize=(10, 6))
            for func_name in df["func"].unique():
                data_by_func = df[(df["func"] == func_name) & (df["size"] > 1e5)].sort_values("size")
                if not data_by_func.empty:
                    plt.plot(data_by_func["size"], data_by_func[metric], label=func_name, marker='o')
            plt.title(f"{metric.upper()} vs Size (Large Inputs Only)")
            plt.xlabel("Input Size (>1e5)")
            plt.ylabel(metric)
            plt.legend()
            plt.grid(True)
            plt.xscale("log")
            if metric != "stddev":
                plt.yscale("log")
            plt.tight_layout()
            plt.savefig(f"{self.benchmarks_plot_dir}/benchmark_{metric}_large.png")
            plt.close()

    def acceleration_plot(self, df: pd.DataFrame) -> None:
        """
        Plots the acceleration of the "Purem" implementation relative to other functions/libraries
        based on their performance for different input sizes. The plot shows acceleration as a
        function of the input size as a logarithmic graph, detailing the relative performance
        gain or loss.

        This function is intended to visualize and compare performance metrics for computational
        functions with respect to a "Purem" baseline. It ensures that shared input sizes
        are used for comparison and applies smoothing (Savitzky-Golay filter) to the acceleration
        curve for better interpretability.

        :param df: A pandas DataFrame containing benchmarking results with the following
            columns:
            - "func": The name of the function or library being benchmarked.
            - "size": The size of the input data for the benchmark.
            - "ops": The respective operations per second or performance metric for each
              function.

        :return: None
        """
        func_names = df["func"].unique()

        assert any("Purem" in name for name in func_names), "Purem is missed!"

        df_purem = df[df["func"].str.contains("Purem")].sort_values("size")

        plt.figure(figsize=(14, 8))

        for func_name in func_names:
            if "Purem" in func_name:
                continue

            df_other = df[df["func"] == func_name].sort_values("size")
            common_sizes = np.intersect1d(df_purem["size"], df_other["size"])

            if len(common_sizes) == 0:
                continue

            purem_ops = df_purem[df_purem["size"].isin(common_sizes)]["ops"].values
            other_ops = df_other[df_other["size"].isin(common_sizes)]["ops"].values

            acceleration = purem_ops / other_ops

            try:
                if len(acceleration) >= 5:
                    acceleration_smooth = savgol_filter(acceleration, window_length=5, polyorder=2)
                else:
                    acceleration_smooth = acceleration
            except Exception as e:
                warnings.warn(f"Smoothing skipped: {e}")
                acceleration_smooth = acceleration

            plt.plot(common_sizes, acceleration_smooth, marker='o', label=f"Purem vs {func_name.partition(':')[2].strip()}")

        plt.xscale('log')
        plt.yscale('log')
        plt.axhline(y=1, color='black', linestyle='--', linewidth=1, label='Equal Performance (x1)')
        plt.xlabel('Input Size', fontsize=14)
        plt.ylabel('Acceleration (Purem / Other)', fontsize=14)
        plt.title('Purem Acceleration Relative to Other Libraries (Production Level)', fontsize=16)
        plt.grid(True, which="both", linestyle='--', linewidth=0.6)
        plt.legend(fontsize=12)
        plt.tight_layout()
        plt.savefig(f"{self.benchmarks_plot_dir}/benchmark_acceleration_large.png")
        plt.close()

    def markdown_data_frame(self) -> pd.DataFrame:
        """
        Converts benchmark data from files into a structured pandas DataFrame.

        This method processes a collection of JSON files that contain benchmark
        results. It reads the files, extracts relevant statistical information
        about the benchmarking process, and formats this data into a structured
        pandas DataFrame. Each record in the resultant DataFrame represents a
        specific benchmark test and includes its relevant metadata and statistical
        metrics.

        :return:
            A pandas DataFrame where each row corresponds to a benchmark with
            columns that include details such as 'Elements', 'Function', 'OPS',
            'Min Time (s)', 'Max Time (s)', 'Mean Time (s)', and 'Std Dev'.
        :rtype:
            pd.DataFrame
        """
        files = self.load_benchmarks_files()

        records_markdown = []

        for filepath in files:
            with open(filepath, "r") as f:
                data = json.load(f)

            filepath_split = filepath.split("_")

            for b in data['benchmarks']:
                size = self.extract_size_from_filepath(filepath)
                func_name = b['params']['func_name']
                stats = b['stats']
                records_markdown.append({
                    "Elements": size,
                    "Function": func_name,
                    "OPS": stats["ops"],
                    "Min Time (s)": stats["min"],
                    "Max Time (s)": stats["max"],
                    "Mean Time (s)": stats["mean"],
                    "Std Dev": stats["stddev"]
                })

        return pd.DataFrame(records_markdown)

    def markdown_save(self, df: pd.DataFrame) -> None:
        """
        Saves a markdown file summarizing benchmark results in a human-readable format. The function processes
        a pandas DataFrame containing benchmark data, formats specific columns for scientific notation or fixed-point
        notation, and groups the results by the "Elements" column. Each group is written as a separate section
        in the markdown file, where the file is saved to the benchmarks directory.

        :param df: A pandas DataFrame containing benchmark results. Expected columns include "Elements",
            "OPS", "Min Time (s)", "Max Time (s)", "Mean Time (s)", and "Std Dev".
        :return: None
        """
        df = df.sort_values(by=["Elements", "OPS"], ascending=[True, False])

        df["Min Time (s)"] = df["Min Time (s)"].map(lambda x: f"{x:.2e}")
        df["Max Time (s)"] = df["Max Time (s)"].map(lambda x: f"{x:.2e}")
        df["Mean Time (s)"] = df["Mean Time (s)"].map(lambda x: f"{x:.2e}")
        df["Std Dev"] = df["Std Dev"].map(lambda x: f"{x:.2e}")
        df["OPS"] = df["OPS"].map(lambda x: f"{x:.2f}")

        grouped = df.groupby('Elements')

        with open(f"{self.benchmarks_plot_dir}/benchmarks_table.md", "w") as f:
            f.write("# Benchmark Results\n\n")
            for size, group in grouped:
                f.write(f"### Elements: {size}\n\n")
                f.write(group.drop(columns=["Elements"]).to_markdown(index=False))
                f.write("\n\n")