mirror of
https://github.com/koloideal/Argenta.git
synced 2026-06-10 10:05:28 +03:00
161 lines
5.4 KiB
Python
161 lines
5.4 KiB
Python
__all__ = ["Benchmark", "Benchmarks", "BenchmarkResult", "BenchmarkGroupResult"]
|
|
|
|
import gc
|
|
import io
|
|
import statistics
|
|
import time
|
|
from contextlib import redirect_stdout
|
|
from dataclasses import dataclass
|
|
from typing import Callable, override
|
|
|
|
from .exceptions import BenchmarkNotFound, BenchmarksNotFound, BenchmarksWithSameNameAlreadyExists
|
|
|
|
FuncForBenchmark = Callable[[], None]
|
|
MILLISECONDS_IN_SECONDS = 1000
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class BenchmarkResult:
|
|
type_: str
|
|
name: str
|
|
description: str
|
|
iterations: int
|
|
is_gc_disabled: bool
|
|
avg_time: float
|
|
median_time: float
|
|
std_dev: float
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class BenchmarkGroupResult:
|
|
type_: str
|
|
iterations: int
|
|
is_gc_disabled: bool
|
|
benchmark_results: list[BenchmarkResult]
|
|
|
|
|
|
class Benchmark:
|
|
def __init__(self, func: FuncForBenchmark, *, type_: str, name: str, description: str) -> None:
|
|
self.func = func
|
|
self.type_ = type_
|
|
self.name = name
|
|
self.description = description
|
|
|
|
def single_run(self) -> float:
|
|
with redirect_stdout(io.StringIO()):
|
|
start = time.perf_counter()
|
|
self.func()
|
|
end = time.perf_counter()
|
|
return (end - start) * MILLISECONDS_IN_SECONDS
|
|
|
|
def multiple_runs(self, iterations: int, is_gc_disabled: bool = False) -> tuple[float, ...]:
|
|
run_attempts: list[float] = []
|
|
if is_gc_disabled:
|
|
was_gc_enabled = gc.isenabled()
|
|
gc.disable()
|
|
for _ in range(iterations):
|
|
run_attempts.append(self.single_run())
|
|
if was_gc_enabled:
|
|
gc.enable()
|
|
gc.collect()
|
|
return tuple(run_attempts)
|
|
else:
|
|
for _ in range(iterations):
|
|
run_attempts.append(self.single_run())
|
|
return tuple(run_attempts)
|
|
|
|
@override
|
|
def __repr__(self) -> str:
|
|
return f"Benchmark<{self.type_=}, {self.name=}, {self.description=}>"
|
|
|
|
@override
|
|
def __str__(self) -> str:
|
|
return f"benchmark {self.name} with type {self.type_}"
|
|
|
|
|
|
class Benchmarks:
|
|
def __init__(self, *benchmarks: Benchmark) -> None:
|
|
self._benchmarks: list[Benchmark] = list(benchmarks)
|
|
self._benchmarks_grouped_by_type: dict[str, list[Benchmark]] = {}
|
|
self._benchmarks_paired_by_name: dict[str, Benchmark] = {}
|
|
|
|
def register(
|
|
self, type_: str, description: str = ""
|
|
) -> Callable[[FuncForBenchmark], FuncForBenchmark]:
|
|
def decorator(func: FuncForBenchmark) -> FuncForBenchmark:
|
|
benchmark = Benchmark(
|
|
func,
|
|
type_=type_,
|
|
name=func.__name__,
|
|
description=description or f"description for {func.__name__} with type {type_}",
|
|
)
|
|
if self._benchmarks_paired_by_name.get(func.__name__):
|
|
raise BenchmarksWithSameNameAlreadyExists(func.__name__)
|
|
|
|
self._benchmarks_paired_by_name[func.__name__] = benchmark
|
|
self._benchmarks.append(benchmark)
|
|
self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark)
|
|
return func
|
|
|
|
return decorator
|
|
|
|
def run_benchmark_by_name(
|
|
self, name: str, iterations: int = 100, is_gc_disables: bool = False
|
|
) -> BenchmarkResult:
|
|
benchmark = self.get_benchmark_by_name(name)
|
|
if not benchmark:
|
|
raise BenchmarkNotFound(name)
|
|
run_attempts: tuple[float, ...] = benchmark.multiple_runs(iterations, is_gc_disables)
|
|
|
|
avg = round(statistics.mean(run_attempts), 4)
|
|
median = round(statistics.median(run_attempts), 4)
|
|
std_dev = round(statistics.stdev(run_attempts) if len(run_attempts) > 1 else 0, 4)
|
|
|
|
return BenchmarkResult(
|
|
type_=benchmark.type_,
|
|
name=benchmark.name,
|
|
description=benchmark.description,
|
|
iterations=iterations,
|
|
is_gc_disabled=is_gc_disables,
|
|
avg_time=avg,
|
|
median_time=median,
|
|
std_dev=std_dev,
|
|
)
|
|
|
|
def run_benchmarks_by_type(
|
|
self, type_: str, iterations: int = 100, is_gc_disabled: bool = False
|
|
) -> BenchmarkGroupResult:
|
|
benchmarks = self.get_benchmarks_by_type(type_)
|
|
if not benchmarks:
|
|
raise BenchmarksNotFound(type_)
|
|
benchmark_results: list[BenchmarkResult] = []
|
|
|
|
for benchmark in benchmarks:
|
|
benchmark_results.append(
|
|
self.run_benchmark_by_name(benchmark.name, iterations, is_gc_disabled)
|
|
)
|
|
|
|
return BenchmarkGroupResult(
|
|
type_=type_,
|
|
iterations=iterations,
|
|
is_gc_disabled=is_gc_disabled,
|
|
benchmark_results=benchmark_results,
|
|
)
|
|
|
|
def run_benchmarks_grouped_by_type(
|
|
self, iterations: int = 100, is_gc_disabled: bool = False
|
|
) -> list[BenchmarkGroupResult]:
|
|
results: list[BenchmarkGroupResult] = []
|
|
for type_, _ in self._benchmarks_grouped_by_type.items():
|
|
results.append(self.run_benchmarks_by_type(type_, iterations, is_gc_disabled))
|
|
return results
|
|
|
|
def get_benchmarks_by_type(self, type_: str) -> list[Benchmark]:
|
|
return self._benchmarks_grouped_by_type.get(type_, [])
|
|
|
|
def get_benchmark_by_name(self, name: str) -> Benchmark | None:
|
|
return self._benchmarks_paired_by_name.get(name)
|
|
|
|
def get_types(self) -> set[str]:
|
|
return set(self._benchmarks_grouped_by_type.keys())
|