mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-24 03:33:38 +00:00
213 lines
6.6 KiB
Python
213 lines
6.6 KiB
Python
|
from hachoir.core.tools import humanDurationNanosec
|
||
|
from math import floor
|
||
|
from time import time
|
||
|
|
||
|
|
||
|
class BenchmarkError(Exception):
|
||
|
"""
|
||
|
Error during benchmark, use str(err) to format it as string.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, message):
|
||
|
Exception.__init__(self, "Benchmark internal error: %s" % message)
|
||
|
|
||
|
|
||
|
class BenchmarkStat:
|
||
|
"""
|
||
|
Benchmark statistics. This class automatically computes minimum value,
|
||
|
maximum value and sum of all values.
|
||
|
|
||
|
Methods:
|
||
|
- append(value): append a value
|
||
|
- getMin(): minimum value
|
||
|
- getMax(): maximum value
|
||
|
- getSum(): sum of all values
|
||
|
- __len__(): get number of elements
|
||
|
- __nonzero__(): isn't empty?
|
||
|
"""
|
||
|
|
||
|
def __init__(self):
|
||
|
self._values = []
|
||
|
|
||
|
def append(self, value):
|
||
|
self._values.append(value)
|
||
|
try:
|
||
|
self._min = min(self._min, value)
|
||
|
self._max = max(self._max, value)
|
||
|
self._sum += value
|
||
|
except AttributeError:
|
||
|
self._min = value
|
||
|
self._max = value
|
||
|
self._sum = value
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self._values)
|
||
|
|
||
|
def __bool__(self):
|
||
|
return bool(self._values)
|
||
|
|
||
|
def getMin(self):
|
||
|
return self._min
|
||
|
|
||
|
def getMax(self):
|
||
|
return self._max
|
||
|
|
||
|
def getSum(self):
|
||
|
return self._sum
|
||
|
|
||
|
|
||
|
class Benchmark:
|
||
|
|
||
|
def __init__(self, max_time=5.0,
|
||
|
min_count=5, max_count=None, progress_time=1.0):
|
||
|
"""
|
||
|
Constructor:
|
||
|
- max_time: Maximum wanted duration of the whole benchmark
|
||
|
(default: 5 seconds, minimum: 1 second).
|
||
|
- min_count: Minimum number of function calls to get good statistics
|
||
|
(defaut: 5, minimum: 1).
|
||
|
- progress_time: Time between each "progress" message
|
||
|
(default: 1 second, minimum: 250 ms).
|
||
|
- max_count: Maximum number of function calls (default: no limit).
|
||
|
- verbose: Is verbose? (default: False)
|
||
|
- disable_gc: Disable garbage collector? (default: False)
|
||
|
"""
|
||
|
self.max_time = max(max_time, 1.0)
|
||
|
self.min_count = max(min_count, 1)
|
||
|
self.max_count = max_count
|
||
|
self.progress_time = max(progress_time, 0.25)
|
||
|
self.verbose = False
|
||
|
self.disable_gc = False
|
||
|
|
||
|
def formatTime(self, value):
|
||
|
"""
|
||
|
Format a time delta to string: use humanDurationNanosec()
|
||
|
"""
|
||
|
return humanDurationNanosec(value * 1000000000)
|
||
|
|
||
|
def displayStat(self, stat):
|
||
|
"""
|
||
|
Display statistics to stdout:
|
||
|
- best time (minimum)
|
||
|
- average time (arithmetic average)
|
||
|
- worst time (maximum)
|
||
|
- total time (sum)
|
||
|
|
||
|
Use arithmetic avertage instead of geometric average because
|
||
|
geometric fails if any value is zero (returns zero) and also
|
||
|
because floating point multiplication lose precision with many
|
||
|
values.
|
||
|
"""
|
||
|
average = stat.getSum() / len(stat)
|
||
|
values = (stat.getMin(), average, stat.getMax(), stat.getSum())
|
||
|
values = tuple(self.formatTime(value) for value in values)
|
||
|
print("Benchmark: best=%s average=%s worst=%s total=%s" % values)
|
||
|
|
||
|
def _runOnce(self, func, args, kw):
|
||
|
before = time()
|
||
|
func(*args, **kw)
|
||
|
after = time()
|
||
|
return after - before
|
||
|
|
||
|
def _run(self, func, args, kw):
|
||
|
"""
|
||
|
Call func(*args, **kw) as many times as needed to get
|
||
|
good statistics. Algorithm:
|
||
|
- call the function once
|
||
|
- compute needed number of calls
|
||
|
- and then call function N times
|
||
|
|
||
|
To compute number of calls, parameters are:
|
||
|
- time of first function call
|
||
|
- minimum number of calls (min_count attribute)
|
||
|
- maximum test time (max_time attribute)
|
||
|
|
||
|
Notice: The function will approximate number of calls.
|
||
|
"""
|
||
|
# First call of the benchmark
|
||
|
stat = BenchmarkStat()
|
||
|
diff = self._runOnce(func, args, kw)
|
||
|
best = diff
|
||
|
stat.append(diff)
|
||
|
total_time = diff
|
||
|
|
||
|
# Compute needed number of calls
|
||
|
count = int(floor(self.max_time / diff))
|
||
|
count = max(count, self.min_count)
|
||
|
if self.max_count:
|
||
|
count = min(count, self.max_count)
|
||
|
|
||
|
# Not other call? Just exit
|
||
|
if count == 1:
|
||
|
return stat
|
||
|
estimate = diff * count
|
||
|
if self.verbose:
|
||
|
print("Run benchmark: %s calls (estimate: %s)"
|
||
|
% (count, self.formatTime(estimate)))
|
||
|
|
||
|
display_progress = self.verbose and (1.0 <= estimate)
|
||
|
total_count = 1
|
||
|
while total_count < count:
|
||
|
# Run benchmark and display each result
|
||
|
if display_progress:
|
||
|
print("Result %s/%s: %s (best: %s)" %
|
||
|
(total_count, count,
|
||
|
self.formatTime(diff), self.formatTime(best)))
|
||
|
part = count - total_count
|
||
|
|
||
|
# Will takes more than one second?
|
||
|
average = total_time / total_count
|
||
|
if self.progress_time < part * average:
|
||
|
part = max(int(self.progress_time / average), 1)
|
||
|
for index in range(part):
|
||
|
diff = self._runOnce(func, args, kw)
|
||
|
stat.append(diff)
|
||
|
total_time += diff
|
||
|
best = min(diff, best)
|
||
|
total_count += part
|
||
|
if display_progress:
|
||
|
print("Result %s/%s: %s (best: %s)" %
|
||
|
(count, count,
|
||
|
self.formatTime(diff), self.formatTime(best)))
|
||
|
return stat
|
||
|
|
||
|
def validateStat(self, stat):
|
||
|
"""
|
||
|
Check statistics and raise a BenchmarkError if they are invalid.
|
||
|
Example of tests: reject empty stat, reject stat with only nul values.
|
||
|
"""
|
||
|
if not stat:
|
||
|
raise BenchmarkError("empty statistics")
|
||
|
if not stat.getSum():
|
||
|
raise BenchmarkError("nul statistics")
|
||
|
|
||
|
def run(self, func, *args, **kw):
|
||
|
"""
|
||
|
Run function func(*args, **kw), validate statistics,
|
||
|
and display the result on stdout.
|
||
|
|
||
|
Disable garbage collector if asked too.
|
||
|
"""
|
||
|
|
||
|
# Disable garbarge collector is needed and if it does exist
|
||
|
# (Jython 2.2 don't have it for example)
|
||
|
if self.disable_gc:
|
||
|
try:
|
||
|
import gc
|
||
|
except ImportError:
|
||
|
self.disable_gc = False
|
||
|
if self.disable_gc:
|
||
|
gc_enabled = gc.isenabled()
|
||
|
gc.disable()
|
||
|
else:
|
||
|
gc_enabled = False
|
||
|
|
||
|
# Run the benchmark
|
||
|
stat = self._run(func, args, kw)
|
||
|
if gc_enabled:
|
||
|
gc.enable()
|
||
|
|
||
|
# Validate and display stats
|
||
|
self.validateStat(stat)
|
||
|
self.displayStat(stat)
|