Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

week-end project is still a thing

  • Loading branch information...
commit 7b3c839fe730a6135cdc3af77a77a3bf4496af69 1 parent 01b2bf7
@cyberdelia authored
Showing with 1,225 additions and 2 deletions.
  1. +2 −1  .gitignore
  2. +19 −0 metrology/__init__.py
  3. +6 −0 metrology/exceptions.py
  4. +6 −0 metrology/instruments/__init__.py
  5. +19 −0 metrology/instruments/counter.py
  6. +40 −0 metrology/instruments/gauge.py
  7. +119 −0 metrology/instruments/histogram.py
  8. +69 −0 metrology/instruments/meter.py
  9. +98 −0 metrology/instruments/timer.py
  10. +63 −0 metrology/registry.py
  11. +30 −0 metrology/reporter/__init__.py
  12. +14 −0 metrology/reporter/graphite.py
  13. +14 −0 metrology/reporter/librato.py
  14. 0  metrology/reporter/logger.py
  15. +4 −0 metrology/stats/__init__.py
  16. +59 −0 metrology/stats/ewma.py
  17. +110 −0 metrology/stats/sample.py
  18. +65 −0 metrology/stats/snapshot.py
  19. +1 −0  setup.py
  20. +31 −0 tests/instruments/test_counter.py
  21. +71 −0 tests/instruments/test_gauge.py
  22. +116 −0 tests/instruments/test_histogram.py
  23. +38 −0 tests/instruments/test_meter.py
  24. +37 −0 tests/instruments/test_timer.py
  25. +52 −0 tests/stats/test_ewma.py
  26. +57 −0 tests/stats/test_sample.py
  27. +29 −0 tests/stats/test_snapshot.py
  28. +26 −0 tests/test_metrology.py
  29. +26 −0 tests/test_registry.py
  30. +4 −1 tox.ini
View
3  .gitignore
@@ -13,4 +13,5 @@ coverage/
.tox/
.DS_Store
.idea
-.venv
+.venv
+htmlcov/
View
19 metrology/__init__.py
@@ -0,0 +1,19 @@
+from metrology.registry import registry
+
+
+class Metrology(object):
+ @classmethod
+ def get(cls, name):
+ return registry.get(name)
+
+ @classmethod
+ def counter(cls, name):
+ return registry.counter(name)
+
+ @classmethod
+ def meter(cls, name):
+ return registry.meter(name)
+
+ @classmethod
+ def gauge(cls, name, gauge):
+ return registry.gauge(name, gauge)
View
6 metrology/exceptions.py
@@ -0,0 +1,6 @@
+class MetrologyException(Exception):
+ pass
+
+
+class ArgumentException(MetrologyException):
+ pass
View
6 metrology/instruments/__init__.py
@@ -0,0 +1,6 @@
+# -*- flake8: noqa -*-
+from metrology.instruments.counter import Counter
+from metrology.instruments.gauge import Gauge
+from metrology.instruments.histogram import Histogram, HistogramUniform, HistogramExponentiallyDecaying
+from metrology.instruments.meter import Meter
+from metrology.instruments.timer import Timer, UtilizationTimer
View
19 metrology/instruments/counter.py
@@ -0,0 +1,19 @@
+from atomic import Atomic
+
+
+class Counter(object):
+ def __init__(self):
+ self._count = Atomic(0)
+
+ def increment(self, value=1):
+ self._count.value += value
+
+ def decrement(self, value=1):
+ self._count.value -= value
+
+ def clear(self):
+ self._count.value = 0
+
+ @property
+ def count(self):
+ return self._count.value
View
40 metrology/instruments/gauge.py
@@ -0,0 +1,40 @@
+from __future__ import division
+
+import math
+
+from atomic import Atomic
+
+
+class Gauge(object):
+ def value(self):
+ raise NotImplementedError
+
+
+class RatioGauge(Gauge):
+ def numerator(self):
+ raise NotImplementedError
+
+ def denominator(self):
+ raise NotImplementedError
+
+ def value(self):
+ d = self.denominator()
+ if math.isnan(d) or math.isinf(d) or d == 0.0 or d == 0:
+ return float('nan')
+ return self.numerator() / d
+
+
+class PercentGauge(RatioGauge):
+ def value(self):
+ value = super(PercentGauge, self).value()
+ return value * 100
+
+
+class ToggleGauge(Gauge):
+ _value = Atomic(1)
+
+ def value(self):
+ try:
+ return self._value.get()
+ finally:
+ self._value.set(0)
View
119 metrology/instruments/histogram.py
@@ -0,0 +1,119 @@
+from __future__ import division
+
+from atomic import Atomic
+
+from metrology.stats.sample import UniformSample, ExponentiallyDecayingSample
+
+
+class Histogram(object):
+ DEFAULT_SAMPLE_SIZE = 1028
+ DEFAULT_ALPHA = 0.015
+
+ def __init__(self, sample):
+ self.sample = sample
+ self.counter = Atomic(0)
+ self.minimum = Atomic()
+ self.maximum = Atomic()
+ self.sum = Atomic(0)
+ self.var = Atomic([-1, 0])
+
+ def clear(self):
+ self.sample.clear()
+ self.counter.value = 0
+ self.minimum.value = None
+ self.maximum.value = None
+ self.sum.value = 0
+ self.var.value = [-1, 0]
+
+ def update(self, value):
+ with self.counter:
+ self.counter.value += 1
+ self.sample.update(value)
+ self.max = value
+ self.min = value
+ with self.sum:
+ self.sum.value += value
+ self.update_variance(value)
+
+ @property
+ def snapshot(self):
+ return self.sample.snapshot()
+
+ @property
+ def count(self):
+ return self.counter.value
+
+ def get_max(self):
+ if self.counter.value > 0:
+ return self.maximum.value
+ return 0.0
+
+ def set_max(self, potential_max):
+ done = False
+ while not done:
+ current_max = self.maximum.value
+ done = (current_max is not None and current_max >= potential_max) \
+ or self.maximum.compare_and_swap(current_max, potential_max)
+
+ max = property(get_max, set_max)
+
+ def get_min(self):
+ if self.counter.value > 0:
+ return self.minimum.value
+ return 0.0
+
+ def set_min(self, potential_min):
+ done = False
+ while not done:
+ current_min = self.minimum.value
+ done = (current_min is not None and current_min <= potential_min) \
+ or self.minimum.compare_and_swap(current_min, potential_min)
+
+ min = property(get_min, set_min)
+
+ @property
+ def mean(self):
+ if self.counter.value > 0:
+ return self.sum.value / self.counter.value
+ return 0.0
+
+ @property
+ def stddev(self):
+ if self.counter.value > 0:
+ return self.var.value
+ return 0.0
+
+ @property
+ def variance(self):
+ if self.counter.value <= 1:
+ return 0.0
+ return self.var.value[1] / (self.counter.value - 1)
+
+ def update_variance(self, value):
+ with self.var:
+ old_values = self.var.value
+ if old_values[0] == -1:
+ new_values = (value, 0)
+ else:
+ old_m = old_values[0]
+ old_s = old_values[1]
+
+ new_m = old_m + ((value - old_m) / self.counter.value)
+ new_s = old_s + ((value - old_m) * (value - new_m))
+
+ new_values = (new_m, new_s)
+
+ self.var.value = new_values
+ return new_values
+
+
+class HistogramUniform(Histogram):
+ def __init__(self):
+ sample = UniformSample(self.DEFAULT_SAMPLE_SIZE)
+ super(HistogramUniform, self).__init__(sample)
+
+
+class HistogramExponentiallyDecaying(Histogram):
+ def __init__(self):
+ sample = ExponentiallyDecayingSample(self.DEFAULT_SAMPLE_SIZE, self.DEFAULT_ALPHA)
+ super(HistogramExponentiallyDecaying, self).__init__(sample)
View
69 metrology/instruments/meter.py
@@ -0,0 +1,69 @@
+import time
+from threading import Thread
+
+from atomic import Atomic
+
+from metrology.stats import EWMA
+
+
+class Meter(object):
+ def __init__(self, average_class=EWMA):
+ self.counter = Atomic(0)
+ self.start_time = time.time()
+
+ self.m1_rate = EWMA.m1()
+ self.m5_rate = EWMA.m5()
+ self.m15_rate = EWMA.m15()
+
+ self.running = True
+
+ def timer():
+ while self.running:
+ time.sleep(average_class.INTERVAL)
+ self.tick()
+
+ self.thread = Thread(target=timer)
+ self.thread.start()
+
+ @property
+ def count(self):
+ return self.counter.value
+
+ def clear(self):
+ self.counter.value = 0
+ self.start_time = time.time()
+
+ self.m1_rate.clear()
+ self.m5_rate.clear()
+ self.m15_rate.clear()
+
+ def mark(self, value=1):
+ with self.counter:
+ self.counter.value += value
+ self.m1_rate.update(value)
+ self.m5_rate.update(value)
+ self.m15_rate.update(value)
+
+ def tick(self):
+ self.m1_rate.tick()
+ self.m5_rate.tick()
+ self.m15_rate.tick()
+
+ def one_minute_rate(self):
+ return self.m1_rate.rate
+
+ def five_minute_rate(self):
+ return self.m5_rate.rate
+
+ def fifteen_minute_rate(self):
+ return self.m15_rate.rate
+
+ def mean_rate(self):
+ if self.counter.value == 0:
+ return 0.0
+ else:
+ elapsed = time.time() - self.start_time
+ return self.counter.value / elapsed
+
+ def stop(self):
+ self.running = False
View
98 metrology/instruments/timer.py
@@ -0,0 +1,98 @@
+import time
+
+from metrology.instruments.histogram import HistogramExponentiallyDecaying
+from metrology.instruments.meter import Meter
+
+
+class Timer(object):
+ def __init__(self, histogram=HistogramExponentiallyDecaying):
+ self.meter = Meter()
+ self.histogram = histogram()
+
+ def clear(self):
+ self.meter.clear()
+ self.histogram.clear()
+
+ def update(self, duration):
+ if duration >= 0:
+ self.meter.mark()
+ self.histogram.update(duration)
+
+ @property
+ def snapshot(self):
+ return self.histogram.snapshot
+
+ def __enter__(self):
+ self.start_time = time.time()
+ return self
+
+ def __exit__(self, type, value, callback):
+ self.update(time.time() - self.start_time)
+
+ @property
+ def count(self):
+ return self.histogram.count
+
+ @property
+ def one_minute_rate(self):
+ return self.meter.one_minute_rate()
+
+ def five_minute_rate(self):
+ return self.meter.five_minute_rate()
+
+ def fifteen_minute_rate(self):
+ return self.meter.fifteen_minute_rate()
+
+ @property
+ def mean_rate(self):
+ return self.meter.mean_rate
+
+ @property
+ def min(self):
+ return self.histogram.min
+
+ @property
+ def max(self):
+ return self.histogram.max
+
+ @property
+ def mean(self):
+ return self.histogram.mean
+
+ @property
+ def stddev(self):
+ return self.histogram.stddev
+
+ def stop(self):
+ self.meter.stop()
+
+
+class UtilizationTimer(Timer):
+ def __init__(self):
+ super(UtilizationTimer, self).__init__()
+ self.duration_meter = Meter()
+
+ def clear(self):
+ super(UtilizationTimer, self).clear()
+ self.duration_meter.clear()
+
+ def update(self, duration):
+ super(UtilizationTimer, self).update(duration)
+ if duration >= 0:
+ self.duration_meter.mark(duration)
+
+ def one_minute_utilization(self):
+ return self.duration_meter.one_minute_rate()
+
+ def five_minute_utilization(self):
+ return self.duration_meter.five_minute_rate()
+
+ def fifteen_minute_utilization(self):
+ return self.duration_meter.fifteen_minute_rate()
+
+ def mean_utilization(self):
+ return self.duration_meter.mean_rate()
+
+ def stop(self):
+ super(UtilizationTimer, self).stop()
+ self.duration_meter.stop()
View
63 metrology/registry.py
@@ -0,0 +1,63 @@
+import inspect
+
+from threading import RLock
+
+from metrology.instruments import Counter, Meter
+
+
+class Registry(object):
+ def __init__(self):
+ self.lock = RLock()
+ self.metrics = {}
+
+ def clear(self):
+ with self.lock:
+ for key, metric in list(self.metrics.items()):
+ if hasattr(metric, 'stop'):
+ metric.stop()
+ self.metrics = {}
+
+ def counter(self, name):
+ return self.add_or_get(name, Counter)
+
+ def meter(self, name):
+ return self.add_or_get(name, Meter)
+
+ def gauge(self, name, klass):
+ return self.add_or_get(name, klass)
+
+ def get(self, name):
+ with self.lock:
+ return self.metrics[name]
+
+ def add(self, name, metric):
+ with self.lock:
+ if name in self.metrics:
+ raise
+ else:
+ self.metrics[name] = metric
+
+ def add_or_get(self, name, klass):
+ with self.lock:
+ if name in self.metrics:
+ metric = self.metrics[name]
+ if not isinstance(metric, klass):
+ raise
+ else:
+ return metric
+ else:
+ if inspect.isclass(klass):
+ self.metrics[name] = klass()
+ else:
+ self.metrics[name] = klass
+ return self.metrics[name]
+
+ def stop(self):
+ self.clear()
+
+ def __iter__(self):
+ with self.lock:
+ for name, metric in list(self.metrics.items()):
+ yield name, metric
+
+registry = Registry()
View
30 metrology/reporter/__init__.py
@@ -0,0 +1,30 @@
+# -*- flake8: noqa -*-
+import time
+
+from threading import Thread
+
+from metrology.reporter.graphite import GraphiteReporter
+from metrology.reporter.librato import LibratoReporter
+from metrology.reporter.logger import LoggerReporter
+
+
+class Reporter(Thread):
+ def __init__(self, *args, **options):
+ self.registry = options.get('registry', registry)
+ self.interval = options.get('interval', 60)
+ super(Reporter, self).__init__()
+
+ def start(self):
+ self.running = True
+ super(Reporter, self).start()
+
+ def stop(self):
+ self.running = False
+
+ def run(self):
+ while self.running:
+ time.sleep(self.interval)
+ self.write()
+
+ def write(self):
+ raise NotImplementedError
View
14 metrology/reporter/graphite.py
@@ -0,0 +1,14 @@
+from metrology.reporter import Reporter
+
+
+class GraphiteReporter(Reporter):
+ def __init__(self, host, port, **options):
+ self.host = host
+ self.port = port
+
+ self.prefix = options.get('prefix')
+ super(GraphiteReporter, self).__init__(**options)
+
+ def write(self):
+ for name, metric in self.registry:
+ pass
View
14 metrology/reporter/librato.py
@@ -0,0 +1,14 @@
+from metrology.reporter import Reporter
+
+
+class LibratoReporter(Reporter):
+ def __init__(self, email, token, **options):
+ self.email = email
+ self.token = token
+
+ self.prefix = options.get('prefix')
+ super(LibratoReporter, self).__init__(**options)
+
+ def write(self):
+ for name, metric in self.registry:
+ pass
View
0  metrology/reporter/logger.py
No changes.
View
4 metrology/stats/__init__.py
@@ -0,0 +1,4 @@
+# -*- flake8: noqa -*-
+from metrology.stats.ewma import EWMA
+from metrology.stats.sample import UniformSample, ExponentiallyDecayingSample
+from metrology.stats.snapshot import Snapshot
View
59 metrology/stats/ewma.py
@@ -0,0 +1,59 @@
+import math
+
+from atomic import Atomic
+
+
+class EWMA(object):
+ INTERVAL = 5.0
+ SECONDS_PER_MINUTE = 60.0
+
+ ONE_MINUTE = 1
+ FIVE_MINUTES = 5
+ FIFTEEN_MINUTES = 15
+
+ M1_ALPHA = 1 - math.exp(-INTERVAL / SECONDS_PER_MINUTE / ONE_MINUTE)
+ M5_ALPHA = 1 - math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIVE_MINUTES)
+ M15_ALPHA = 1 - math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIFTEEN_MINUTES)
+
+ @classmethod
+ def m1(cls):
+ return EWMA(cls.M1_ALPHA, cls.INTERVAL)
+
+ @classmethod
+ def m5(cls):
+ return EWMA(cls.M5_ALPHA, cls.INTERVAL)
+
+ @classmethod
+ def m15(cls):
+ return EWMA(cls.M15_ALPHA, cls.INTERVAL)
+
+ def __init__(self, alpha, interval):
+ self.alpha = alpha
+ self.interval = interval
+
+ self.initialized = False
+ self._rate = 0.0
+ self._uncounted = Atomic(0)
+
+ def clear(self):
+ self.initialized = False
+ self._rate = 0.0
+ self._uncounted.value = 0
+
+ def update(self, value):
+ with self._uncounted:
+ self._uncounted.value += value
+
+ def tick(self):
+ count = self._uncounted.swap(0)
+ instant_rate = count / self.interval
+
+ if self.initialized:
+ self._rate += self.alpha * (instant_rate - self._rate)
+ else:
+ self._rate = instant_rate
+ self.initialized = True
+
+ @property
+ def rate(self):
+ return self._rate
View
110 metrology/stats/sample.py
@@ -0,0 +1,110 @@
+import math
+import random
+import time
+
+from atomic import Atomic
+from bintrees.rbtree import RBTree
+from threading import RLock
+
+from metrology.stats.snapshot import Snapshot
+
+
+class UniformSample(object):
+ def __init__(self, reservoir_size):
+ self.counter = Atomic(0)
+ self.values = [0 for i in range(reservoir_size)]
+
+ def clear(self):
+ self.values = [0 for i in range(len(self.values))]
+ self.counter.value = 0
+
+ def size(self):
+ count = self.counter.value
+ if count > len(self.values):
+ return len(self.values)
+ return count
+
+ def __len__(self):
+ return self.size
+
+ def snapshot(self):
+ return Snapshot(self.values[0:self.size()])
+
+ def update(self, value):
+ with self.counter:
+ self.counter.value += 1
+ new_count = self.counter.value
+
+ if new_count <= len(self.values):
+ self.values[new_count - 1] = value
+ else:
+ index = random.uniform(0, new_count)
+ if index < len(self.values):
+ self.values[int(index)] = value
+
+
+class ExponentiallyDecayingSample(object):
+ RESCALE_THRESHOLD = 60 * 60
+
+ def __init__(self, reservoir_size, alpha):
+ self.values = RBTree()
+ self.counter = Atomic(0)
+ self.next_scale_time = Atomic(0)
+ self.alpha = alpha
+ self.reservoir_size = reservoir_size
+ self.lock = RLock()
+ self.clear()
+
+ def clear(self):
+ with self.lock:
+ self.values.clear()
+ self.counter.value = 0
+ self.next_scale_time.value = time.time() + self.RESCALE_THRESHOLD
+ self.start_time = time.time()
+
+ def size(self):
+ count = self.counter.value
+ if count < self.reservoir_size:
+ return count
+ return self.reservoir_size
+
+ def __len__(self):
+ return self.size()
+
+ def snapshot(self):
+ with self.lock:
+ return Snapshot(list(self.values.values()))
+
+ def weight(self, timestamp):
+ return math.exp(self.alpha * timestamp)
+
+ def rescale(self, now, next_time):
+ if self.next_scale_time.compare_and_swap(next_time, now + self.RESCALE_THRESHOLD):
+ with self.lock:
+ old_start_time = self.start_time
+ self.start_time = time.time()
+ for key in list(self.values.keys()):
+ value = self.values.remove(key)
+ self.values[key * math.exp(-self.alpha * (self.start_time - old_start_time))] = value
+
+ def update(self, value, timestamp=None):
+ if not timestamp:
+ timestamp = time.time()
+ with self.lock:
+ priority = self.weight(timestamp - self.start_time) / random.random()
+ with self.counter:
+ self.counter.value += 1
+ new_count = self.counter.value
+
+ if math.isnan(priority):
+ return
+
+ if new_count <= self.reservoir_size:
+ self.values[priority] = value
+ else:
+ first_priority = self.values.root.key
+ if first_priority < priority:
+ if priority in self.values:
+ self.values[priority] = value
+ if not self.values.remove(first_priority):
+ first_priority = self.values.root()
View
65 metrology/stats/snapshot.py
@@ -0,0 +1,65 @@
+import math
+
+from metrology.exceptions import ArgumentException
+
+
+class Snapshot(object):
+ MEDIAN_Q = 0.5
+ P75_Q = 0.75
+ P95_Q = 0.95
+ P98_Q = 0.98
+ P99_Q = 0.99
+ P999_Q = 0.999
+
+ def __init__(self, values):
+ values.sort()
+ self.values = values
+
+ def value(self, quantile):
+ if 0.0 > quantile > 1.0:
+ raise ArgumentException("Quantile must be between 0.0 and 1.0")
+
+ if not self.values:
+ return 0.0
+
+ pos = quantile * (len(self.values) + 1)
+
+ if pos < 1:
+ return self.values[0]
+
+ if pos >= len(self.values):
+ return self.values[-1]
+
+ lower = self.values[int(pos) - 1]
+ upper = self.values[int(pos)]
+ return lower + (pos - math.floor(pos)) * (upper - lower)
+
+ def size(self):
+ return len(self.values)
+
+ def __len__(self):
+ return self.size()
+
+ @property
+ def median(self):
+ return self.value(self.MEDIAN_Q)
+
+ @property
+ def percentile_75th(self):
+ return self.value(self.P75_Q)
+
+ @property
+ def percentile_95th(self):
+ return self.value(self.P95_Q)
+
+ @property
+ def percentile_98th(self):
+ return self.value(self.P98_Q)
+
+ @property
+ def percentile_99th(self):
+ return self.value(self.P99_Q)
+
+ @property
+ def percentile_999th(self):
+ return self.value(self.P999_Q)
View
1  setup.py
@@ -13,6 +13,7 @@
zip_safe=False,
install_requires=[
'atomic',
+ 'bintrees'
],
include_package_data=True,
classifiers=[
View
31 tests/instruments/test_counter.py
@@ -0,0 +1,31 @@
+from unittest import TestCase
+
+from metrology.instruments.counter import Counter
+
+
+class CounterTest(TestCase):
+ def setUp(self):
+ self.counter = Counter()
+
+ def test_increment(self):
+ self.counter.increment()
+ self.assertEqual(1, self.counter.count)
+
+ def test_increment_more(self):
+ self.counter.increment(10)
+ self.assertEqual(10, self.counter.count)
+
+ def test_clear(self):
+ self.counter.increment(10)
+ self.counter.clear()
+ self.assertEqual(0, self.counter.count)
+
+ def test_decrement(self):
+ self.counter.increment(10)
+ self.counter.decrement()
+ self.assertEqual(9, self.counter.count)
+
+ def test_decrement_more(self):
+ self.counter.increment(10)
+ self.counter.decrement(9)
+ self.assertEqual(1, self.counter.count)
View
71 tests/instruments/test_gauge.py
@@ -0,0 +1,71 @@
+import math
+
+from unittest import TestCase
+
+from metrology.instruments.gauge import Gauge, RatioGauge, PercentGauge, ToggleGauge # noqa
+
+
+class DummyGauge(Gauge):
+ def value(self):
+ return "wow"
+
+
+class DummyRatioGauge(RatioGauge):
+ def __init__(self, numerator, denominator):
+ self.num = numerator
+ self.den = denominator
+
+ def numerator(self):
+ return self.num
+
+ def denominator(self):
+ return self.den
+
+
+class DummyPercentGauge(DummyRatioGauge, PercentGauge):
+ pass
+
+
+class GaugeTest(TestCase):
+ def setUp(self):
+ self.gauge = DummyGauge()
+
+ def test_return_value(self):
+ self.assertEqual(self.gauge.value(), "wow")
+
+
+class RatioGaugeTest(TestCase):
+ def test_ratio(self):
+ gauge = DummyRatioGauge(2, 4)
+ self.assertEqual(gauge.value(), 0.5)
+
+ def test_divide_by_zero(self):
+ gauge = DummyRatioGauge(100, 0)
+ self.assertTrue(math.isnan(gauge.value()))
+
+ def test_divide_by_infinite(self):
+ gauge = DummyRatioGauge(100, float('inf'))
+ self.assertTrue(math.isnan(gauge.value()))
+
+ def test_divide_by_nan(self):
+ gauge = DummyRatioGauge(100, float('nan'))
+ self.assertTrue(math.isnan(gauge.value()))
+
+
+class PercentGaugeTest(TestCase):
+ def test_percentage(self):
+ gauge = DummyPercentGauge(2, 4)
+ self.assertEqual(gauge.value(), 50.)
+
+ def test_with_nan(self):
+ gauge = DummyPercentGauge(2, 0)
+ self.assertTrue(math.isnan(gauge.value()))
+
+
+class ToggleGaugeTest(TestCase):
+ def test_return_one_then_zero(self):
+ gauge = ToggleGauge()
+ self.assertEqual(gauge.value(), 1)
+ self.assertEqual(gauge.value(), 0)
+ self.assertEqual(gauge.value(), 0)
+ self.assertEqual(gauge.value(), 0)
View
116 tests/instruments/test_histogram.py
@@ -0,0 +1,116 @@
+from threading import Thread
+from unittest import TestCase
+
+from metrology.instruments.histogram import HistogramUniform, HistogramExponentiallyDecaying
+
+
+class HistogramTest(TestCase):
+ def test_uniform_sample_min(self):
+ histogram = HistogramUniform()
+ histogram.update(5)
+ histogram.update(10)
+ self.assertEqual(5, histogram.min)
+
+ def test_uniform_sample_max(self):
+ histogram = HistogramUniform()
+ histogram.update(5)
+ histogram.update(10)
+ self.assertEqual(10, histogram.max)
+
+ def test_uniform_sample_mean(self):
+ histogram = HistogramUniform()
+ histogram.update(5)
+ histogram.update(10)
+ self.assertEqual(7.5, histogram.mean)
+
+ def test_uniform_sample_mean_threaded(self):
+ histogram = HistogramUniform()
+
+ def update():
+ for i in range(100):
+ histogram.update(5)
+ histogram.update(10)
+ for thread in [Thread(target=update) for i in range(10)]:
+ thread.start()
+ thread.join()
+ self.assertEqual(7.5, histogram.mean)
+
+ def test_uniform_sample_2000(self):
+ histogram = HistogramUniform()
+ for i in range(2000):
+ histogram.update(i)
+ self.assertEqual(1999, histogram.max)
+
+ def test_uniform_sample_snapshot(self):
+ histogram = HistogramUniform()
+ for i in range(100):
+ histogram.update(i)
+ snapshot = histogram.snapshot
+ self.assertEqual(49.5, snapshot.median)
+
+ def test_uniform_sample_snapshot_threaded(self):
+ histogram = HistogramUniform()
+
+ def update():
+ for i in range(100):
+ histogram.update(i)
+ for thread in [Thread(target=update) for i in range(10)]:
+ thread.start()
+ thread.join()
+ snapshot = histogram.snapshot
+ self.assertEqual(49.5, snapshot.median)
+
+ def test_exponential_sample_min(self):
+ histogram = HistogramExponentiallyDecaying()
+ histogram.update(5)
+ histogram.update(10)
+ self.assertEqual(5, histogram.min)
+
+ def test_exponential_sample_max(self):
+ histogram = HistogramExponentiallyDecaying()
+ histogram.update(5)
+ histogram.update(10)
+ self.assertEqual(10, histogram.max)
+
+ def test_exponential_sample_mean(self):
+ histogram = HistogramExponentiallyDecaying()
+ histogram.update(5)
+ histogram.update(10)
+ self.assertEqual(7.5, histogram.mean)
+
+ def test_exponential_sample_mean_threaded(self):
+ histogram = HistogramExponentiallyDecaying()
+
+ def update():
+ for i in range(100):
+ histogram.update(5)
+ histogram.update(10)
+ for thread in [Thread(target=update) for i in range(10)]:
+ thread.start()
+ thread.join()
+ self.assertEqual(7.5, histogram.mean)
+
+ def test_exponential_sample_2000(self):
+ histogram = HistogramExponentiallyDecaying()
+ for i in range(2000):
+ histogram.update(i)
+ self.assertEqual(1999, histogram.max)
+
+ def test_exponential_sample_snapshot(self):
+ histogram = HistogramExponentiallyDecaying()
+ for i in range(100):
+ histogram.update(i)
+ snapshot = histogram.snapshot
+ self.assertEqual(49.5, snapshot.median)
+
+ def test_exponential_sample_snapshot_threaded(self):
+ histogram = HistogramExponentiallyDecaying()
+
+ def update():
+ for i in range(100):
+ histogram.update(i)
+ for thread in [Thread(target=update) for i in range(10)]:
+ thread.start()
+ thread.join()
+ snapshot = histogram.snapshot
+ self.assertEqual(49.5, snapshot.median)
View
38 tests/instruments/test_meter.py
@@ -0,0 +1,38 @@
+from threading import Thread
+from unittest import TestCase
+
+from metrology.instruments.meter import Meter
+
+
+class MeterTest(TestCase):
+ def setUp(self):
+ self.meter = Meter()
+
+ def test_meter(self):
+ self.meter.mark()
+ self.assertEqual(1, self.meter.count)
+
+ def test_blank_meter(self):
+ self.assertEqual(0, self.meter.count)
+ self.assertEqual(0.0, self.meter.mean_rate())
+
+ def test_meter_value(self):
+ self.meter.mark(3)
+ self.assertEqual(3, self.meter.count)
+
+ def test_one_minute_rate(self):
+ self.meter.mark(1000)
+ self.meter.tick()
+ self.assertEqual(200, self.meter.one_minute_rate())
+
+ def test_meter_threaded(self):
+ def mark():
+ for i in range(100):
+ self.meter.mark()
+ for thread in [Thread(target=mark) for i in range(10)]:
+ thread.start()
+ thread.join()
+ self.assertEqual(1000, self.meter.count)
+
+ def tearDown(self):
+ self.meter.stop()
View
37 tests/instruments/test_timer.py
@@ -0,0 +1,37 @@
+import time
+
+from unittest import TestCase
+
+from metrology.instruments.timer import Timer, UtilizationTimer
+
+
+class TimerTest(TestCase):
+ def setUp(self):
+ self.timer = Timer()
+
+ def tearDown(self):
+ self.timer.stop()
+
+ def test_timer(self):
+ for i in range(3):
+ with self.timer:
+ time.sleep(0.1)
+ self.assertAlmostEqual(0.1, self.timer.mean, 1)
+ self.assertAlmostEqual(0.1, self.timer.snapshot.median, 1)
+
+
+class UtilizationTimerTest(TestCase):
+ def setUp(self):
+ self.timer = UtilizationTimer()
+
+ def tearDown(self):
+ self.timer.stop()
+
+ def test_timer(self):
+ for i in range(5):
+ self.timer.update(0.10)
+ self.timer.update(0.15)
+ self.timer.meter.tick()
+ self.timer.duration_meter.tick()
+
+ self.assertAlmostEqual(0.25, self.timer.one_minute_utilization(), 1)
View
52 tests/stats/test_ewma.py
@@ -0,0 +1,52 @@
+from unittest import TestCase
+
+from metrology.stats.ewma import EWMA
+
+
+class EwmaTest(TestCase):
+ def _one_minute(self, ewma):
+ for i in range(12):
+ ewma.tick()
+
+ def test_one_minute_ewma(self):
+ ewma = EWMA.m1()
+ ewma.update(3)
+ ewma.tick()
+ self.assertAlmostEqual(ewma.rate, 0.6, places=6)
+
+ self._one_minute(ewma)
+ self.assertAlmostEqual(ewma.rate, 0.22072766, places=6)
+
+ self._one_minute(ewma)
+ self.assertAlmostEqual(ewma.rate, 0.08120117, places=6)
+
+ def test_five_minute_ewma(self):
+ ewma = EWMA.m5()
+ ewma.update(3)
+ ewma.tick()
+ self.assertAlmostEqual(ewma.rate, 0.6, places=6)
+
+ self._one_minute(ewma)
+ self.assertAlmostEqual(ewma.rate, 0.49123845, places=6)
+
+ self._one_minute(ewma)
+ self.assertAlmostEqual(ewma.rate, 0.40219203, places=6)
+
+ def test_fifteen_minute_ewma(self):
+ ewma = EWMA.m15()
+ ewma.update(3)
+ ewma.tick()
+ self.assertAlmostEqual(ewma.rate, 0.6, places=6)
+
+ self._one_minute(ewma)
+ self.assertAlmostEqual(ewma.rate, 0.56130419, places=6)
+
+ self._one_minute(ewma)
+ self.assertAlmostEqual(ewma.rate, 0.52510399, places=6)
+
+ def test_clear_ewma(self):
+ ewma = EWMA.m15()
+ ewma.update(3)
+ ewma.tick()
+ ewma.clear()
+ self.assertAlmostEqual(ewma.rate, 0)
View
57 tests/stats/test_sample.py
@@ -0,0 +1,57 @@
+from unittest import TestCase
+
+from metrology.stats.sample import UniformSample, ExponentiallyDecayingSample
+
+
+class UniformSampleTest(TestCase):
+ def test_sample(self):
+ sample = UniformSample(100)
+ for i in range(1000):
+ sample.update(i)
+ snapshot = sample.snapshot()
+ self.assertEqual(sample.size(), 100)
+ self.assertEqual(snapshot.size(), 100)
+
+ for value in snapshot.values:
+ self.assertTrue(value < 1000.0)
+ self.assertTrue(value >= 0.0)
+
+
+class ExponentiallyDecayingSampleTest(TestCase):
+ def test_sample_1000(self):
+ sample = ExponentiallyDecayingSample(100, 0.99)
+ for i in range(1000):
+ sample.update(i)
+ self.assertEqual(sample.size(), 100)
+ snapshot = sample.snapshot()
+ self.assertEqual(snapshot.size(), 100)
+
+ for value in snapshot.values:
+ self.assertTrue(value < 1000.0)
+ self.assertTrue(value >= 0.0)
+
+ def test_sample_10(self):
+ sample = ExponentiallyDecayingSample(100, 0.99)
+ for i in range(10):
+ sample.update(i)
+ self.assertEqual(sample.size(), 10)
+
+ snapshot = sample.snapshot()
+ self.assertEqual(snapshot.size(), 10)
+
+ for value in snapshot.values:
+ self.assertTrue(value < 10.0)
+ self.assertTrue(value >= 0.0)
+
+ def test_sample_100(self):
+ sample = ExponentiallyDecayingSample(1000, 0.01)
+ for i in range(100):
+ sample.update(i)
+ self.assertEqual(sample.size(), 100)
+
+ snapshot = sample.snapshot()
+ self.assertEqual(snapshot.size(), 100)
+
+ for value in snapshot.values:
+ self.assertTrue(value < 100.0)
+ self.assertTrue(value >= 0.0)
View
29 tests/stats/test_snapshot.py
@@ -0,0 +1,29 @@
+from unittest import TestCase
+
+from metrology.stats.snapshot import Snapshot
+
+
+class SnapshotTest(TestCase):
+ def setUp(self):
+ self.snapshot = Snapshot([5, 1, 2, 3, 4])
+
+ def test_median(self):
+ self.assertAlmostEqual(self.snapshot.median, 3, 1)
+
+ def test_75th_percentile(self):
+ self.assertAlmostEqual(self.snapshot.percentile_75th, 4.5, 1)
+
+ def test_95th_percentile(self):
+ self.assertAlmostEqual(self.snapshot.percentile_95th, 5.0, 1)
+
+ def test_98th_percentile(self):
+ self.assertAlmostEqual(self.snapshot.percentile_98th, 5.0, 1)
+
+ def test_99th_percentile(self):
+ self.assertAlmostEqual(self.snapshot.percentile_99th, 5.0, 1)
+
+ def test_999th_percentile(self):
+ self.assertAlmostEqual(self.snapshot.percentile_999th, 5.0, 1)
+
+ def test_size(self):
+ self.assertEqual(self.snapshot.size(), 5)
View
26 tests/test_metrology.py
@@ -0,0 +1,26 @@
+from unittest import TestCase
+
+from metrology import Metrology
+from metrology.instruments.gauge import Gauge
+from metrology.registry import registry
+
+
+class MetrologyTest(TestCase):
+ def setUp(self):
+ registry.clear()
+
+ def tearDown(self):
+ registry.clear()
+
+ def test_get(self):
+ Metrology.counter('test')
+ self.assertTrue(Metrology.get('test') is not None)
+
+ def test_counter(self):
+ self.assertTrue(Metrology.counter('test') is not None)
+
+ def test_meter(self):
+ self.assertTrue(Metrology.meter('test') is not None)
+
+ def test_gauge(self):
+ self.assertTrue(Metrology.gauge('test', Gauge) is not None)
View
26 tests/test_registry.py
@@ -0,0 +1,26 @@
+from unittest import TestCase
+
+from metrology.registry import Registry
+from metrology.instruments.gauge import Gauge
+
+
+class DummyGauge(Gauge):
+ def value(self):
+ return "wow"
+
+
+class RegistryTest(TestCase):
+ def setUp(self):
+ self.registry = Registry()
+
+ def tearDown(self):
+ self.registry.stop()
+
+ def test_counter(self):
+ self.assertTrue(self.registry.counter('test') is not None)
+
+ def test_meter(self):
+ self.assertTrue(self.registry.meter('test') is not None)
+
+ def test_gauge(self):
+ self.assertTrue(self.registry.gauge('test', DummyGauge()) is not None)
View
5 tox.ini
@@ -2,5 +2,8 @@
envlist = py26,py27,py32,pypy
[testenv]
-deps = pytest
+deps =
+ pytest
+ atomic
+ bintrees
commands = py.test
Please sign in to comment.
Something went wrong with that request. Please try again.