diff --git a/README.md b/README.md index 63e2d9c..117ceeb 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,21 @@ Statman.stopwatch('stopwatch-name').read() print(f'event took {Statman.stopwatch('stopwatch-name').read(precision=1)}s to execute') # event took 1.0s to execute ``` +### Stopwatch via Statman Registry with thread safety +Most use cases of statman are thread safe. However, the one that is not is using stopwatch via registry. If you want to use stopwatch via statman in a threaded env, small change. + +``` python +from statman import Statman + +sw1 = SM.stopwatch(name='sw', thread_safe=True) +sw1.start() +# do some expensive operation that you want to measure +sw1.read() + +print(f'event took {sw1.read(precision=1)}s to execute') # event took 1.0s to execute +``` + + ### Stopwatch: Direct Usage (no registry) ``` python from statman import Stopwatch diff --git a/statman/__init__.py b/statman/__init__.py index 61ef0eb..9d3299f 100644 --- a/statman/__init__.py +++ b/statman/__init__.py @@ -4,5 +4,6 @@ from .history import History from .calculation import Calculation from .metric import Metric +from .decorators import timer __all__ = ['Statman', 'Stopwatch'] \ No newline at end of file diff --git a/statman/decorators.py b/statman/decorators.py new file mode 100644 index 0000000..3813cf7 --- /dev/null +++ b/statman/decorators.py @@ -0,0 +1,17 @@ +from .statman import Statman + + +def timer(name): + + def timer_inner_decorator(func): + + def timer_wrapper(*args, **kwargs): + sw = Statman.stopwatch(name=name, enable_history=True, thread_safe=True) + sw.start() + result = func(*args, **kwargs) + sw.stop() + return result + + return timer_wrapper + + return timer_inner_decorator diff --git a/statman/history.py b/statman/history.py index 94baa8a..ebdcb67 100644 --- a/statman/history.py +++ b/statman/history.py @@ -9,8 +9,7 @@ class History(): def __init__(self): self._data = [] - def __str__(self): - pass + # def __str__(self): def append(self, dt: datetime = None, value: float = None) -> str: event = self.create_event(dt=dt, value=value) diff --git a/statman/statman.py b/statman/statman.py index afbce91..76ac272 100644 --- a/statman/statman.py +++ b/statman/statman.py @@ -1,9 +1,11 @@ +import threading +import uuid +from is_numeric import is_numeric from .stopwatch import Stopwatch from .gauge import Gauge from .calculation import Calculation from .rate import Rate from .metric import Metric -from is_numeric import is_numeric _registry = {} @@ -24,8 +26,11 @@ def count() -> int: return len(Statman.metric_registry().keys()) @staticmethod - def stopwatch(name: str = None, autostart: bool = False, initial_delta: float = None, enable_history=False) -> Stopwatch: + def stopwatch(name: str = None, autostart: bool = False, initial_delta: float = None, enable_history=False, thread_safe=False) -> Stopwatch: ''' Returns a stopwatch instance. If there is a registered stopwatch with this name, return it. If there is no registered stopwatch with this name, create a new instance, register it, and return it. ''' + if thread_safe: + return Statman._stopwatch_threadsafe(name=name, autostart=autostart, initial_delta=initial_delta, enable_history=enable_history) + sw = Statman.metric_registry().get(name) if not sw: @@ -36,6 +41,29 @@ def stopwatch(name: str = None, autostart: bool = False, initial_delta: float = return sw + @staticmethod + def _stopwatch_threadsafe(name: str = None, autostart: bool = False, initial_delta: float = None, enable_history=False): + parent_sw = Statman.metric_registry().get(name) + + if not parent_sw: + lock = threading.Lock() + with lock: + parent_sw = Statman.metric_registry().get(name) + if not parent_sw: + parent_sw = Stopwatch(name=name, autostart=False, enable_history=enable_history) + if not name is None: + Statman.register(name, parent_sw) + print(f'_stopwatch_threadsafe-{name}: add to registry {parent_sw=}') + + child_name = f'{name}.{uuid.uuid4()}' + child_sw = Stopwatch(name=child_name, + autostart=autostart, + initial_delta=initial_delta, + enable_history=enable_history, + history=parent_sw.history) + + return child_sw + @staticmethod def gauge(name: str = None, value: float = 0) -> Gauge: ''' Returns a stopwatch instance. If there is a registered stopwatch with this name, return it. If there is no registered stopwatch with this name, create a new instance, register it, and return it. ''' @@ -186,11 +214,10 @@ def refresh(self): Statman.gauge(statman_key).value = value else: print(f'skipping non-numeric value {key=} {value=} {statman_key=}') + elif isinstance(value, (int, float)): + print(f'skipping non-dictionary, numeric {result=}') else: - if isinstance(value, int) or isinstance(value, float): - print(f'skipping non-dictionary, numeric {result=}') - else: - print(f'skipping non-dictionary, non-numeric {result=}') + print(f'skipping non-dictionary, non-numeric {result=}') except Exception as e: print(f'failed to execute refresh method [{self._name}][{e}]') diff --git a/statman/stopwatch.py b/statman/stopwatch.py index 8ff35da..02764e7 100644 --- a/statman/stopwatch.py +++ b/statman/stopwatch.py @@ -1,6 +1,6 @@ import time -from .metric import Metric from statman.history import History +from .metric import Metric class Stopwatch(Metric): @@ -10,12 +10,15 @@ class Stopwatch(Metric): _read_units = 'ms' _history = None - def __init__(self, name=None, autostart=False, initial_delta=None, enable_history=False): + def __init__(self, name=None, autostart=False, initial_delta=None, enable_history=False, history: History = None): super().__init__(name=name) self.reset() self._initial_delta = initial_delta if enable_history: - self._history = History() + if history: + self._history = history + else: + self._history = History() if autostart: self.start() @@ -44,6 +47,7 @@ def stop(self, units: str = 's', precision: int = None) -> float: if self.history: self.history.append(value=self.value) + print(f'sw.stop {self.name=} {self.history=} {self.history.count()=}') return self.read(units=units, precision=precision) @@ -55,7 +59,7 @@ def restart(self): self.reset() self.start() - def read(self, units: str = 's', precision: int = None) -> float: + def read(self, precision: int = None, units: str = 's') -> float: delta = None if self._start_time: stop_time = None diff --git a/statman/tests/test_statman.py b/statman/tests/test_statman.py index cdc6ed5..6d3752c 100644 --- a/statman/tests/test_statman.py +++ b/statman/tests/test_statman.py @@ -12,12 +12,54 @@ def run_before_and_after_tests(data): print('reset registry between tests') Statman.reset() - def test_create_stopwatch_directly(self): - from statman import Stopwatch - sw = Stopwatch() - sw.start() + def log(self, message): + print(f'{TestStatman} {message}') + + +class TestGaugeViaStatman(TestStatman): + + def test_create_gauge_via_statman_package(self): + import statman + gauge = statman.Gauge() + gauge.value = 1 + self.assertEqual(gauge.value, 1) + + def test_access_gauge_through_registry(self): + g1 = Statman.gauge('g1') + g1.value = 1 + + Statman.gauge(name='g2', value=2).increment() + + Statman.gauge('g3') + Statman.gauge('g3').value = 20 + Statman.gauge('g3').increment(10) + + self.assertEqual(Statman.gauge('g1').name, 'g1') + self.assertEqual(Statman.gauge('g1').value, 1) + self.assertEqual(Statman.gauge('g2').value, 3) + self.assertEqual(Statman.gauge('g3').value, 30) + + def test_report(self): + Statman.gauge('g1').value = 1 + Statman.gauge('g2').value = 2 + Statman.gauge('g3') + Statman.gauge('g4').increment() + Statman.stopwatch('sw1', autostart=True, enable_history=True) time.sleep(1) - self.assertAlmostEqual(sw.read(), 1, delta=0.1) + Statman.stopwatch('sw1').stop() + Statman.stopwatch('sw1', autostart=True, enable_history=True) + Statman.stopwatch('sw2', autostart=True, enable_history=True) + Statman.stopwatch('sw3', autostart=False, enable_history=True) + Statman.stopwatch('sw4', autostart=True, enable_history=True) + time.sleep(2) + Statman.stopwatch('sw1').stop() + Statman.stopwatch('sw4').stop() + + message = Statman.report(output_stdout=False, log_method=self.log) + print('raw message:', message) + + +class TestStopwatchViaStatman(TestStatman): def test_create_stopwatch_via_statman_package(self): import statman @@ -26,6 +68,13 @@ def test_create_stopwatch_via_statman_package(self): time.sleep(1) self.assertAlmostEqual(sw.read(), 1, delta=0.1) + def test_create_stopwatch_directly(self): + from statman import Stopwatch + sw = Stopwatch() + sw.start() + time.sleep(1) + self.assertAlmostEqual(sw.read(), 1, delta=0.1) + def test_create_stopwatch_via_statman_constructor(self): sw = Statman.stopwatch() sw.start() @@ -134,48 +183,42 @@ def test_manually_registry(self): time.sleep(1) self.assertAlmostEqual(sw.read(), 1, delta=0.1) - def test_create_gauge_via_statman_package(self): - import statman - gauge = statman.Gauge() - gauge.value = 1 - self.assertEqual(gauge.value, 1) - def test_access_gauge_through_registry(self): - g1 = Statman.gauge('g1') - g1.value = 1 +class TestStopwatchViaStatmanConcurrency(TestStatman): - Statman.gauge(name='g2', value=2).increment() + def test_concurrent_access(self): + from statman import Statman as SM - Statman.gauge('g3') - Statman.gauge('g3').value = 20 - Statman.gauge('g3').increment(10) + sw1 = SM.stopwatch(name='sw', autostart=False, enable_history=True, thread_safe=True) + self.assertIsNotNone(sw1) + sw1.start() - self.assertEqual(Statman.gauge('g1').name, 'g1') - self.assertEqual(Statman.gauge('g1').value, 1) - self.assertEqual(Statman.gauge('g2').value, 3) - self.assertEqual(Statman.gauge('g3').value, 30) + time.sleep(0.5) + + sw2 = SM.stopwatch(name='sw', autostart=False, enable_history=True, thread_safe=True) + self.assertIsNotNone(sw2) + sw2.start() - def test_report(self): - Statman.gauge('g1').value = 1 - Statman.gauge('g2').value = 2 - Statman.gauge('g3') - Statman.gauge('g4').increment() - Statman.stopwatch('sw1', autostart=True, enable_history=True) time.sleep(1) - Statman.stopwatch('sw1').stop() - Statman.stopwatch('sw1', autostart=True, enable_history=True) - Statman.stopwatch('sw2', autostart=True, enable_history=True) - Statman.stopwatch('sw3', autostart=False, enable_history=True) - Statman.stopwatch('sw4', autostart=True, enable_history=True) - time.sleep(2) - Statman.stopwatch('sw1').stop() - Statman.stopwatch('sw4').stop() - message = Statman.report(output_stdout=False, log_method=self.log) - print('raw message:', message) + sw1.stop() - def log(self, message): - print('XX ' + message) + time.sleep(1) + + sw2.stop() + + self.assertIsNot(sw1, sw2) + print(sw1.history) + print(sw2.history) + self.assertIs(sw1.history, sw2.history) + self.assertAlmostEqual(sw1.value, 1.5, places=0) + self.assertAlmostEqual(sw2.value, 2.0, places=0) + + self.assertEqual(sw1.history.count(), 2) + self.assertEqual(sw2.history.count(), 2) + + +class TestCalculationViaStatman(TestStatman): def test_calculation_metric(self): Statman.stopwatch('sw').start() diff --git a/statman/tests/test_timer_decorator.py b/statman/tests/test_timer_decorator.py new file mode 100644 index 0000000..389e4b0 --- /dev/null +++ b/statman/tests/test_timer_decorator.py @@ -0,0 +1,34 @@ +import unittest +import time +import statman +from statman import Statman +from statman.stopwatch import Stopwatch + + +class TestStopwatch(unittest.TestCase): + + def test_decorator_stopwatch(self): + delay = 7 + result = self.sut('moto', delay=delay) + self.assertEqual(result, 'hello moto') + + self.assertIsNotNone(Statman.stopwatch('sut.timer')) + self.assertEqual(Statman.stopwatch('sut.timer').name, 'sut.timer') + self.assertAlmostEqual(Statman.stopwatch('sut.timer').value, delay, delta=0.1) + + @statman.timer(name="sut.timer") + def sut(self, n: str, delay: int = 1): + msg = f'hello {n}' + print(msg) + time.sleep(delay) + return msg + + def test_decorator_stopwatch_2(self): + delay = 5 + result = self.sut('moto2', delay=delay) + self.assertEqual(result, 'hello moto2') + # self.assertAlmostEqual(stopwatch.read(), test_time_s, delta=self._accepted_variance) + + self.assertIsNotNone(Statman.stopwatch('sut.timer')) + self.assertEqual(Statman.stopwatch('sut.timer').name, 'sut.timer') + self.assertAlmostEqual(Statman.stopwatch('sut.timer').value, delay, delta=0.1)