Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions statman/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
from .history import History
from .calculation import Calculation
from .metric import Metric
from .decorators import timer

__all__ = ['Statman', 'Stopwatch']
17 changes: 17 additions & 0 deletions statman/decorators.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions statman/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 33 additions & 6 deletions statman/statman.py
Original file line number Diff line number Diff line change
@@ -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 = {}

Expand All @@ -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:
Expand All @@ -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. '''
Expand Down Expand Up @@ -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}]')
Expand Down
12 changes: 8 additions & 4 deletions statman/stopwatch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import time
from .metric import Metric
from statman.history import History
from .metric import Metric


class Stopwatch(Metric):
Expand All @@ -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()

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
121 changes: 82 additions & 39 deletions statman/tests/test_statman.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
34 changes: 34 additions & 0 deletions statman/tests/test_timer_decorator.py
Original file line number Diff line number Diff line change
@@ -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)