├── .coveragerc
├── .gitignore
├── .travis.yml
├── HISTORY.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── dev-requirements.txt
├── setup.cfg
├── setup.py
├── tox.ini
└── yunomi
├── __init__.py
├── compat.py
├── core
├── __init__.py
├── counter.py
├── histogram.py
├── meter.py
├── metrics_registry.py
└── timer.py
├── stats
├── __init__.py
├── ewma.py
├── exp_decay_sample.py
├── snapshot.py
└── uniform_sample.py
└── tests
├── __init__.py
├── test_EWMA.py
├── test_counter.py
├── test_exp_decay_sample_test.py
├── test_histogram.py
├── test_meter.py
├── test_metrics_registry.py
├── test_snapshot.py
├── test_timer.py
├── test_uniform_sample.py
└── util.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit =
3 | yunomi/tests/*
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | _trial_temp
3 | build
4 | dist
5 | .tox
6 | yunomi.egg-info
7 | .coverage
8 | htmlcov
9 | MANIFEST
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - '2.7'
4 |
5 | before_install: pip install tox
6 |
7 | script: tox
8 |
--------------------------------------------------------------------------------
/HISTORY.rst:
--------------------------------------------------------------------------------
1 | .. :changelog:
2 |
3 | History
4 | -------
5 |
6 | 0.3.0 (2013-07-27)
7 | ++++++++++++++++++
8 |
9 | - Python 3.3 is now supported additionally to 2.6 and 2.7.
10 | - Decorators don’t eat return values anymore.
11 | - Meters can be reset now.
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2013
2 | David Reid
3 | Richie Zeng
4 | Ying Li
5 | Hynek Schlawack
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining
8 | a copy of this software and associated documentation files (the
9 | "Software"), to deal in the Software without restriction, including
10 | without limitation the rights to use, copy, modify, merge, publish,
11 | distribute, sublicense, and/or sell copies of the Software, and to
12 | permit persons to whom the Software is furnished to do so, subject to
13 | the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be
16 | included in all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst LICENSE HISTORY.rst
2 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | YUNOMI: Y U NO MEASURE IT
2 | =========================
3 |
4 | .. image:: https://badge.fury.io/py/yunomi.png
5 | :target: http://badge.fury.io/py/yunomi
6 |
7 | .. image:: https://secure.travis-ci.org/dreid/yunomi.png?branch=master
8 | :target: http://travis-ci.org/dreid/yunomi
9 |
10 | As in:
11 |
12 | It has performance implications, Y U NO MEASURE IT!?
13 |
14 | Yunomi provides insights to the internal behavior of an application, providing useful statistics and metrics on selected portions of your code.
15 | It’s a Python port of the core portion of a `Java Metrics library by Coda Hale `_.
16 |
17 | Stop guessing, start measuring:
18 |
19 | .. code-block:: bash
20 |
21 | $ pip install yunomi
22 |
23 | Core Features
24 | -------------
25 |
26 | Counter
27 | +++++++
28 |
29 | Simple interface to increment and decrement a value.
30 | For example, this can be used to measure the total number of jobs sent to the queue, as well as the pending (not yet complete) number of jobs in the queue.
31 | Simply increment the counter when an operation starts and decrement it when it completes.
32 |
33 | Meter
34 | +++++
35 |
36 | Measures the rate of events over time.
37 | Useful to track how often a certain portion of your application gets requests so you can set resources accordingly.
38 | Tracks the mean rate (the overall rate since the meter was reset) and the rate statistically significant regarding only events that have happened in the last 1, 5, and 15 minutes (Exponentially weighted moving average).
39 |
40 | Histogram
41 | +++++++++
42 |
43 | Measures the statistical distribution of values in a data stream.
44 | Keeps track of minimum, maximum, mean, standard deviation, etc.
45 | It also measures median, 75th, 90th, 95th, 98th, 99th, and 99.9th percentiles.
46 | An example use case would be for looking at the number of daily logins for 99 percent of your days, ignoring outliers.
47 |
48 | Timer
49 | +++++
50 |
51 | A useful combination of the Meter and the Histogram letting you measure the rate that a portion of code is called and a distribution of the duration of an operation.
52 | You can see, for example, how often your code hits the database and how long those operations tend to take.
53 |
54 |
55 | Examples
56 | --------
57 |
58 | Decorators
59 | ++++++++++
60 |
61 | The simplest and easiest way to use the yunomi library.
62 |
63 | Counter
64 | ^^^^^^^
65 |
66 | You can use the `count_calls` decorator to count the number of times a function is called.
67 |
68 | .. code-block:: pycon
69 |
70 | >>> from yunomi import counter, count_calls
71 | >>> @count_calls
72 | ... def test():
73 | ... pass
74 | ...
75 | >>> for i in xrange(10):
76 | ... test()
77 | ...
78 | >>> print counter("test_calls").get_count()
79 | 10
80 |
81 | Timer
82 | ^^^^^
83 |
84 | You can use the `time_calls` decorator to time the execution of a function and get distributtion data from it.
85 |
86 | .. code-block:: pycon
87 |
88 | >>> import time
89 | >>> from yunomi import timer, time_calls
90 | >>> @time_calls
91 | ... def test():
92 | ... time.sleep(0.1)
93 | ...
94 | >>> for i in xrange(10):
95 | ... test()
96 | ...
97 | >>> print timer("test_calls").get_mean()
98 | 0.100820207596
99 |
100 |
101 | Requirements
102 | ------------
103 |
104 | Yunomi has no external dependencies and runs on PyPy and Python 2.6, 2.7, and 3.3.
105 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | mock
2 | unittest2
3 | pytest
4 | pytest-cov
5 | tox
6 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | # we're pure-python
3 | universal = 1
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name="yunomi",
5 | version="0.3.0",
6 | description=("A Python metrics library with rate, statistical "
7 | "distribution, and timing information."),
8 | long_description=(open('README.rst').read() + '\n\n' +
9 | open('HISTORY.rst').read()),
10 | maintainer="David Reid",
11 | maintainer_email="dreid@dreid.org",
12 | url="https://github.com/dreid/yunomi",
13 | license="MIT",
14 | classifiers=[
15 | "Development Status :: 4 - Beta",
16 | "Intended Audience :: Developers",
17 | "License :: OSI Approved :: MIT License",
18 | "Operating System :: OS Independent",
19 | "Programming Language :: Python",
20 | "Programming Language :: Python :: 2.6",
21 | "Programming Language :: Python :: 2.7",
22 | "Programming Language :: Python :: 3.3",
23 | "Programming Language :: Python :: Implementation :: CPython",
24 | "Programming Language :: Python :: Implementation :: PyPy",
25 | "Topic :: Software Development :: Libraries :: Python Modules",
26 |
27 | ],
28 | packages=find_packages()
29 | )
30 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py26, py27, pypy, py33
3 |
4 | [testenv]
5 | deps=
6 | mock
7 | pytest
8 | pytest-cov
9 | unittest2
10 | commands=
11 | py.test --cov=yunomi yunomi
12 |
13 | [testenv:py33]
14 | deps=
15 | mock
16 | pytest
17 | pytest-cov
18 | unittest2py3k
19 |
--------------------------------------------------------------------------------
/yunomi/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from yunomi.core.metrics_registry import (MetricsRegistry, counter, histogram,
4 | meter, timer, dump_metrics,
5 | count_calls, meter_calls, hist_calls,
6 | time_calls)
7 | from yunomi.core.counter import Counter
8 | from yunomi.core.histogram import Histogram
9 | from yunomi.core.meter import Meter
10 | from yunomi.core.timer import Timer
11 |
12 | __all__ = ['MetricsRegistry', 'Counter', 'Histogram', 'Meter', 'Timer',
13 | 'counter', 'histogram', 'meter', 'timer', 'dump_metrics',
14 | 'count_calls', 'meter_calls', 'hist_calls', 'time_calls']
15 |
--------------------------------------------------------------------------------
/yunomi/compat.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | import sys
4 |
5 |
6 | if sys.version_info < (3, 0):
7 | _PY3 = False
8 | xrange = xrange
9 |
10 | def dict_item_iter(d):
11 | """
12 | Return an iterator over the dict items.
13 | """
14 | return d.iteritems()
15 | else:
16 | _PY3 = True
17 | xrange = range
18 |
19 | def dict_item_iter(d):
20 | """
21 | Return an iterator over the dict items.
22 | """
23 | return d.items()
24 |
25 |
26 | __all__ = [
27 | _PY3, xrange, dict_item_iter
28 | ]
29 |
--------------------------------------------------------------------------------
/yunomi/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreid/yunomi/1aa3f166843613331b38e231264ffc3ac40e8094/yunomi/core/__init__.py
--------------------------------------------------------------------------------
/yunomi/core/counter.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | class Counter(object):
4 | """
5 | A counter method that increments and decrements.
6 | """
7 | def __init__(self):
8 | """
9 | Create a new instance of a L{Counter}.
10 | """
11 | self._count = 0
12 |
13 | def inc(self, n = 1):
14 | """
15 | Increment the counter by I{n}.
16 |
17 | @type n: C{int}
18 | @param n: the amount to be incremented
19 | """
20 | self._count += n
21 |
22 | def dec(self, n = 1):
23 | """
24 | Decrement the counter by I{n}.
25 |
26 | @type n: C{int}
27 | @param n: the amount to be decrement
28 | """
29 | self._count -= n
30 |
31 | def get_count(self):
32 | """
33 | Returns the count
34 |
35 | @rtype: C{int}
36 | @return: the count
37 | """
38 | return self._count
39 |
40 | def clear(self):
41 | """
42 | Resets the count back to 0.
43 | """
44 | self._count = 0
45 |
--------------------------------------------------------------------------------
/yunomi/core/histogram.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from math import sqrt
4 |
5 | from yunomi.stats.exp_decay_sample import ExponentiallyDecayingSample
6 | from yunomi.stats.uniform_sample import UniformSample
7 |
8 |
9 | class Histogram(object):
10 | """
11 | A metric which calculates the distribution of a value.
12 |
13 | @see: Accurately computing running variance
14 | """
15 | DEFAULT_SAMPLE_SIZE = 1028
16 | DEFAULT_ALPHA = 0.015
17 | count = 0
18 | mean = 0
19 | sum_of_squares = -1.0
20 |
21 | def __init__(self, sample):
22 | """
23 | Creates a new instance of a L{Histogram}.
24 |
25 | @type sample: L{ExponentiallyDecayingSample} or L{UniformSample}
26 | @param sample: an instance of L{ExponentiallyDecayingSample} or
27 | L{UniformSample}
28 | """
29 | self.sample = sample
30 | self.clear()
31 |
32 | @classmethod
33 | def get_biased(klass):
34 | """
35 | Create a new instance of L{Histogram} that uses an L{ExponentiallyDecayingSample}
36 | with sample size L{DEFAULT_SAMPLE_SIZE} and alpha L{DEFAULT_ALPHA}.
37 |
38 | @return: L{Histogram}
39 | """
40 | return klass(ExponentiallyDecayingSample(klass.DEFAULT_SAMPLE_SIZE, klass.DEFAULT_ALPHA))
41 |
42 | @classmethod
43 | def get_uniform(klass):
44 | """
45 | Create a new instance of L{Histogram} that uses an L{UniformSample}
46 | with sample size L{DEFAULT_SAMPLE_SIZE}.
47 |
48 | """
49 | return klass(UniformSample(klass.DEFAULT_SAMPLE_SIZE))
50 |
51 | def clear(self):
52 | """
53 | Resets the values to default.
54 | """
55 | self.sample.clear()
56 | self.max_ = -2147483647.0
57 | self.min_ = 2147483647.0
58 | self.sum_ = 0.0
59 | self.count = 0
60 | self.mean = 0
61 | self.sum_of_squares = -1.0
62 |
63 | def update(self, value):
64 | """
65 | Updates all the fields with the new value (if applicable).
66 |
67 | @type value: C{int}
68 | @param value: the value to update the fields with
69 | """
70 | self.count += 1
71 | self.sample.update(value)
72 | self.set_max(value)
73 | self.set_min(value)
74 | self.sum_ += value
75 | self.update_variance_info(value)
76 |
77 | def get_count(self):
78 | """
79 | The number of values put into the histogram.
80 | """
81 | return self.count
82 |
83 | def get_max(self):
84 | """
85 | The maximum value that has been updated into the histogram.
86 |
87 | @rtype: C{int} or C{float}
88 | @return: the max value
89 | """
90 | if self.get_count() > 0:
91 | return self.max_
92 | return 0.0
93 |
94 | def get_min(self):
95 | """
96 | The minimum value that has been updated into the histogram.
97 |
98 | @rtype: C{int} or C{float}
99 | @return: the min value
100 | """
101 | if self.get_count() > 0:
102 | return self.min_
103 | return 0.0
104 |
105 | def get_mean(self):
106 | """
107 | The average of all the values that have been updated into the
108 | historgram.
109 |
110 | @rtype: C{float}
111 | @return: the average of all the values updated
112 | """
113 | if self.get_count() > 0:
114 | return float(self.sum_) / self.get_count()
115 | return 0.0
116 |
117 | def get_std_dev(self):
118 | """
119 | Returns the standard devation calculated by taking the square root of
120 | the variance, which is updated whenever a new value is added.
121 |
122 | @rtype: C{float}
123 | @return: the standard deviation
124 | """
125 | if self.get_count() > 0:
126 | return sqrt(self.get_variance())
127 | return 0.0
128 |
129 | def get_variance(self):
130 | """
131 | Returns the variance calculated using the sum of squares of deviations
132 | from the mean and the total count, which are both updated whenever a
133 | new value is added.
134 |
135 | @rtype: C{float}
136 | @return: the variance
137 | """
138 | if self.get_count() <= 1:
139 | return 0.0
140 | return self.sum_of_squares / (self.get_count() - 1)
141 |
142 | def get_sum(self):
143 | """
144 | The sum of all the values, updated whenever a value is added. Useful
145 | for computing the mean quickly.
146 |
147 | @rtype: C{int} or C{float}
148 | @return: the sum of all the values
149 | """
150 | return self.sum_
151 |
152 | def get_snapshot(self):
153 | """
154 | Returns a snapshot of the current set of values in the histogram.
155 |
156 | @rtype: L{Snapshot}
157 | @return: the snapshot of the current values
158 | """
159 | return self.sample.get_snapshot()
160 |
161 | def set_max(self, new_max):
162 | """
163 | Checks if a value is greater than the current max. If so, update
164 | I{max_}.
165 |
166 | @type new_max: C{int} or C{float}
167 | @param new_max: the potential new maximum value to check
168 | """
169 | if self.max_ < new_max:
170 | self.max_ = new_max
171 |
172 | def set_min(self, new_min):
173 | """
174 | Checks if a value is less than the current min. If so, update I{min_}.
175 |
176 | @type new_min: C{int} or C{float}
177 | @param new_min: the potential new minimum value to check
178 | """
179 | if self.min_ > new_min:
180 | self.min_ = new_min
181 |
182 | def update_variance_info(self, value):
183 | """
184 | Updates the I{sum_of_squares} and I{mean} whenever a new value is
185 | updated. This makes computing the variance more computationally
186 | efficient.
187 |
188 | @type value: C{int} or C{float}
189 | @param value: the value being added to the histogram
190 | """
191 | old_mean = self.mean
192 | delta = value - old_mean
193 | if self.sum_of_squares == -1.0:
194 | self.mean = value
195 | self.sum_of_squares = 0.0
196 | else:
197 | self.mean += (float(delta) / self.get_count())
198 | self.sum_of_squares += (float(delta) * (value - self.mean))
199 |
--------------------------------------------------------------------------------
/yunomi/core/meter.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from time import time
4 |
5 | from yunomi.stats.ewma import EWMA
6 |
7 |
8 | class Meter(object):
9 | """
10 | A meter metric which measures mean throughput and one-, five-, and fifteen-
11 | minute exponentially-weighted moving average throughputs.
12 |
13 | @see: EMA
14 | """
15 | INTERVAL = 5
16 |
17 | def __init__(self, event_type=""):
18 | """
19 | Creates a new L{Meter} instance.
20 |
21 | @type event_type: C{str}
22 | @param event_type: the plural name of the event the meter is measuring
23 | (e.g., I{"requests"})
24 | """
25 | self.event_type = event_type
26 | self.start_time = time()
27 | self._m1_rate = EWMA.one_minute_EWMA()
28 | self._m5_rate = EWMA.five_minute_EWMA()
29 | self._m15_rate = EWMA.fifteen_minute_EWMA()
30 | self._count = 0
31 |
32 | def clear(self):
33 | """
34 | Resets the meter.
35 | """
36 | self.start_time = time()
37 | self._count = 0
38 | self._m1_rate = EWMA.one_minute_EWMA()
39 | self._m5_rate = EWMA.five_minute_EWMA()
40 | self._m15_rate = EWMA.fifteen_minute_EWMA()
41 |
42 | def get_event_type(self):
43 | """
44 | Returns the event type.
45 |
46 | @rtype: C{str}
47 | @return: the event type
48 | """
49 | return self.event_type
50 |
51 | def _tick(self):
52 | """
53 | Updates the moving averages.
54 | """
55 | self._m1_rate.tick()
56 | self._m15_rate.tick()
57 | self._m5_rate.tick()
58 |
59 | def mark(self, n = 1):
60 | """
61 | Mark the occurrence of a given number of events.
62 |
63 | @type n: C{int}
64 | @param n: number of events
65 | """
66 | self._count += n
67 | self._m1_rate.update(n)
68 | self._m15_rate.update(n)
69 | self._m5_rate.update(n)
70 |
71 | def get_count(self):
72 | """
73 | Return the number of events that have been counted.
74 |
75 | @rtype: C{int}
76 | @return: the total number of events
77 | """
78 | return self._count
79 |
80 | def get_fifteen_minute_rate(self):
81 | """
82 | Get the rate of the L{EWMA} equivalent to a fifteen minute load average.
83 |
84 | @rtype: C{float}
85 | @return: the fifteen minute rate
86 | """
87 | return self._m15_rate.get_rate()
88 |
89 | def get_five_minute_rate(self):
90 | """
91 | Get the rate of the L{EWMA} equivalent to a five minute load average.
92 |
93 | @rtype: C{float}
94 | @return: the five minute rate
95 | """
96 | return self._m5_rate.get_rate()
97 |
98 | def get_one_minute_rate(self):
99 | """
100 | Get the rate of the L{EWMA} equivalent to a one minute load average.
101 |
102 | @rtype: C{float}
103 | @return: the one minute rate
104 | """
105 | return self._m1_rate.get_rate()
106 |
107 | def get_mean_rate(self):
108 | """
109 | Get the overall rate, the total number of events over the time since
110 | the beginning.
111 |
112 | @rtype: C{float}
113 | @return: the mean minute rate
114 | """
115 | if self._count == 0:
116 | return 0.0
117 | else:
118 | elapsed = time() - self.start_time
119 | return float(self._count) / elapsed
120 |
--------------------------------------------------------------------------------
/yunomi/core/metrics_registry.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from time import time
4 | from functools import wraps
5 |
6 | from yunomi.compat import dict_item_iter
7 | from yunomi.core.counter import Counter
8 | from yunomi.core.histogram import Histogram
9 | from yunomi.core.meter import Meter
10 | from yunomi.core.timer import Timer
11 |
12 |
13 | class MetricsRegistry(object):
14 | """
15 | A single interface used to gather metrics on a service. It keeps track of
16 | all the relevant Counters, Meters, Histograms, and Timers. It does not have
17 | a reference back to its service. The service would create a
18 | L{MetricsRegistry} to manage all of its metrics tools.
19 | """
20 | def __init__(self, clock=time):
21 | """
22 | Creates a new L{MetricsRegistry} instance.
23 | """
24 | self._timers = {}
25 | self._meters = {}
26 | self._counters = {}
27 | self._histograms = {}
28 |
29 | self._clock = clock
30 |
31 | def counter(self, key):
32 | """
33 | Gets a counter based on a key, creates a new one if it does not exist.
34 |
35 | @param key: name of the metric
36 | @type key: C{str}
37 |
38 | @return: L{Counter}
39 | """
40 | if key not in self._counters:
41 | self._counters[key] = Counter()
42 | return self._counters[key]
43 |
44 | def histogram(self, key, biased=False):
45 | """
46 | Gets a histogram based on a key, creates a new one if it does not exist.
47 |
48 | @param key: name of the metric
49 | @type key: C{str}
50 |
51 | @return: L{Histogram}
52 | """
53 | if key not in self._histograms:
54 | if biased:
55 | self._histograms[key] = Histogram.get_biased()
56 | else:
57 | self._histograms[key] = Histogram.get_uniform()
58 |
59 | return self._histograms[key]
60 |
61 | def meter(self, key):
62 | """
63 | Gets a meter based on a key, creates a new one if it does not exist.
64 |
65 | @param key: name of the metric
66 | @type key: C{str}
67 |
68 | @return: L{Meter}
69 | """
70 | if key not in self._meters:
71 | self._meters[key] = Meter()
72 | return self._meters[key]
73 |
74 | def timer(self, key):
75 | """
76 | Gets a timer based on a key, creates a new one if it does not exist.
77 |
78 | @param key: name of the metric
79 | @type key: C{str}
80 |
81 | @return: L{Timer}
82 | """
83 | if key not in self._timers:
84 | self._timers[key] = Timer()
85 | return self._timers[key]
86 |
87 | def dump_metrics(self):
88 | """
89 | Formats all the metrics into dicts, and returns a list of all of them
90 |
91 | @return: C{list} of C{dict} of metrics
92 | """
93 | metrics = []
94 |
95 | # format timed stats
96 | for key, timer in dict_item_iter(self._timers):
97 | snapshot = timer.get_snapshot()
98 | for suffix, val in (("avg", timer.get_mean()),
99 | ("max", timer.get_max()),
100 | ("min", timer.get_min()),
101 | ("std_dev", timer.get_std_dev()),
102 | ("15m_rate", timer.get_fifteen_minute_rate()),
103 | ("5m_rate", timer.get_five_minute_rate()),
104 | ("1m_rate", timer.get_one_minute_rate()),
105 | ("mean_rate", timer.get_mean_rate()),
106 | ("75_percentile", snapshot.get_75th_percentile()),
107 | ("98_percentile", snapshot.get_98th_percentile()),
108 | ("99_percentile", snapshot.get_99th_percentile()),
109 | ("999_percentile", snapshot.get_999th_percentile())):
110 | k = "_".join([key, suffix])
111 | _new_metric = {
112 | "type": "float",
113 | "name": k,
114 | "value": val,
115 | }
116 | metrics.append(_new_metric)
117 |
118 | # format meter stats
119 | for key, meter in dict_item_iter(self._meters):
120 | for suffix, val in (("15m_rate", meter.get_fifteen_minute_rate()),
121 | ("5m_rate", meter.get_five_minute_rate()),
122 | ("1m_rate", meter.get_one_minute_rate()),
123 | ("mean_rate", meter.get_mean_rate())):
124 | k = "_".join([key, suffix])
125 | _new_metric = {
126 | "type": "float",
127 | "name": k,
128 | "value": val,
129 | }
130 | metrics.append(_new_metric)
131 |
132 | # format histogram stats
133 | for key, histogram in dict_item_iter(self._histograms):
134 | snapshot = histogram.get_snapshot()
135 | for suffix, val in (("avg", histogram.get_mean()),
136 | ("max", histogram.get_max()),
137 | ("min", histogram.get_min()),
138 | ("std_dev", histogram.get_std_dev()),
139 | ("75_percentile", snapshot.get_75th_percentile()),
140 | ("98_percentile", snapshot.get_98th_percentile()),
141 | ("99_percentile", snapshot.get_99th_percentile()),
142 | ("999_percentile", snapshot.get_999th_percentile())):
143 | k = "_".join([key, suffix])
144 | _new_metric = {
145 | "type": "float",
146 | "name": k,
147 | "value": val,
148 | }
149 | metrics.append(_new_metric)
150 |
151 | # format counter stats
152 | for key, counter in dict_item_iter(self._counters):
153 | k = "_".join([key, "count"])
154 | val = counter.get_count()
155 | _new_metric = {
156 | "type": "int",
157 | "name": k,
158 | "value": val
159 | }
160 | metrics.append(_new_metric)
161 |
162 | # alphabetize
163 | metrics.sort(key=lambda x: x["name"])
164 | return metrics
165 |
166 |
167 | _global_registry = MetricsRegistry()
168 |
169 | counter = _global_registry.counter
170 | histogram = _global_registry.histogram
171 | meter = _global_registry.meter
172 | timer = _global_registry.timer
173 | dump_metrics = _global_registry.dump_metrics
174 |
175 | def count_calls(fn):
176 | """
177 | Decorator to track the number of times a function is called.
178 |
179 | @param fn: the function to be decorated
180 | @type fn: C{func}
181 |
182 | @return: the decorated function
183 | @rtype: C{func}
184 | """
185 | @wraps(fn)
186 | def wrapper(*args):
187 | counter("%s_calls" % fn.__name__).inc()
188 | try:
189 | return fn(*args)
190 | except:
191 | raise
192 | return wrapper
193 |
194 | def meter_calls(fn):
195 | """
196 | Decorator to the rate at which a function is called.
197 |
198 | @param fn: the function to be decorated
199 | @type fn: C{func}
200 |
201 | @return: the decorated function
202 | @rtype: C{func}
203 | """
204 | @wraps(fn)
205 | def wrapper(*args):
206 | meter("%s_calls" % fn.__name__).mark()
207 | try:
208 | return fn(*args)
209 | except:
210 | raise
211 | return wrapper
212 |
213 | def hist_calls(fn):
214 | """
215 | Decorator to check the distribution of return values of a function.
216 |
217 | @param fn: the function to be decorated
218 | @type fn: C{func}
219 |
220 | @return: the decorated function
221 | @rtype: C{func}
222 | """
223 | @wraps(fn)
224 | def wrapper(*args):
225 | _histogram = histogram("%s_calls" % fn.__name__)
226 | try:
227 | rtn = fn(*args)
228 | if type(rtn) in (int, float):
229 | _histogram.update(rtn)
230 | return rtn
231 | except:
232 | raise
233 | return wrapper
234 |
235 | def time_calls(fn):
236 | """
237 | Decorator to time the execution of the function.
238 |
239 | @param fn: the function to be decorated
240 | @type fn: C{func}
241 |
242 | @return: the decorated function
243 | @rtype: C{func}
244 | """
245 | @wraps(fn)
246 | def wrapper(*args):
247 | _timer = timer("%s_calls" % fn.__name__)
248 | start = time()
249 | try:
250 | return fn(*args)
251 | except:
252 | raise
253 | finally:
254 | _timer.update(time() - start)
255 | return wrapper
256 |
--------------------------------------------------------------------------------
/yunomi/core/timer.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from yunomi.stats.snapshot import Snapshot
4 | from yunomi.core.histogram import Histogram
5 | from yunomi.core.meter import Meter
6 |
7 |
8 | class Timer(object):
9 | """
10 | A timer metric which aggregates timing durations and provides duration
11 | statistics, plus throughput statistics via L{Meter}.
12 | """
13 |
14 | def __init__(self):
15 | """
16 | Creates a new L{Timer} instance.
17 | """
18 | self.histogram = Histogram.get_biased()
19 | self.meter = Meter("calls")
20 |
21 | def clear(self):
22 | """
23 | Clears all recorded durations in the histogram.
24 | """
25 | self.histogram.clear()
26 |
27 | def update(self, duration):
28 | """
29 | Updates the L{Histogram} and marks the L{Meter}.
30 |
31 | @type duration: C{int}
32 | @param duration: the duration of an event
33 | """
34 | if duration >= 0:
35 | self.histogram.update(duration)
36 | self.meter.mark()
37 |
38 | def get_count(self):
39 | """
40 | L{Histogram.get_count}
41 | """
42 | return self.histogram.get_count()
43 |
44 | def get_fifteen_minute_rate(self):
45 | """
46 | L{Meter.get_fifteen_minute_rate}
47 | """
48 | return self.meter.get_fifteen_minute_rate()
49 |
50 | def get_five_minute_rate(self):
51 | """
52 | L{Meter.get_five_minute_rate}
53 | """
54 | return self.meter.get_five_minute_rate()
55 |
56 | def get_one_minute_rate(self):
57 | """
58 | L{Meter.get_one_minute_rate}
59 | """
60 | return self.meter.get_one_minute_rate()
61 |
62 | def get_mean_rate(self):
63 | """
64 | L{Meter.get_mean_rate}
65 | """
66 | return self.meter.get_mean_rate()
67 |
68 | def get_max(self):
69 | """
70 | L{Histogram.get_max}
71 | """
72 | return self.histogram.get_max()
73 |
74 | def get_min(self):
75 | """
76 | L{Histogram.get_min}
77 | """
78 | return self.histogram.get_min()
79 |
80 | def get_mean(self):
81 | """
82 | L{Histogram.get_mean}
83 | """
84 | return self.histogram.get_mean()
85 |
86 | def get_std_dev(self):
87 | """
88 | L{Histogram.get_std_dev}
89 | """
90 | return self.histogram.get_std_dev()
91 |
92 | def get_sum(self):
93 | """
94 | L{Histogram.get_sum}
95 | """
96 | return self.histogram.get_sum()
97 |
98 | def get_snapshot(self):
99 | """
100 | L{Histogram.get_snapshot}
101 | """
102 | values = self.histogram.get_snapshot().get_values()
103 | return Snapshot(values)
104 |
105 | def get_event_type(self):
106 | """
107 | L{Meter.get_event_type}
108 | """
109 | return self.meter.get_event_type()
110 |
--------------------------------------------------------------------------------
/yunomi/stats/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreid/yunomi/1aa3f166843613331b38e231264ffc3ac40e8094/yunomi/stats/__init__.py
--------------------------------------------------------------------------------
/yunomi/stats/ewma.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from math import exp
4 | from time import time
5 |
6 |
7 | class EWMA(object):
8 | """
9 | An exponentially-weighted moving average.
10 |
11 | @see: UNIX Load Average Part 1: How It Works
12 | @see: UNIX Load Average Part 2: Not Your Average Average
13 | """
14 | INTERVAL = 5
15 |
16 | def __init__(self, period, interval=None):
17 | """
18 | Create a new EWMA with a specific smoothing constant.
19 |
20 | @type period: C{int}
21 | @param period: the time it takes to reach a given significance level
22 | @type interval: C{int}
23 | @param interval: the expected tick interval, defaults to 5s
24 | """
25 | self.initialized = False
26 | self._period = period
27 | self._interval = (interval or EWMA.INTERVAL)
28 | self._uncounted = 0.0
29 | self._rate = 0.0
30 | self._last_tick = time()
31 |
32 | @classmethod
33 | def one_minute_EWMA(klass):
34 | """
35 | Creates a new EWMA which is equivalent to the UNIX one minute load
36 | average.
37 |
38 | @rtype: L{EWMA}
39 | @return: a one-minute EWMA
40 | """
41 | return klass(60)
42 |
43 | @classmethod
44 | def five_minute_EWMA(klass):
45 | """
46 | Creates a new EWMA which is equivalent to the UNIX five minute load
47 | average.
48 |
49 | @rtype: L{EWMA}
50 | @return: a five-minute EWMA
51 | """
52 | return klass(300)
53 |
54 | @classmethod
55 | def fifteen_minute_EWMA(klass):
56 | """
57 | Creates a new EWMA which is equivalent to the UNIX fifteen minute load
58 | average.
59 |
60 | @rtype: L{EWMA}
61 | @return: a fifteen-minute EWMA
62 | """
63 | return klass(900)
64 |
65 | def update(self, value):
66 | """
67 | Increment the moving average with a new value.
68 |
69 | @type value: C{int} or C{float}
70 | @param value: the new value
71 | """
72 | self._uncounted += value
73 |
74 | def tick(self):
75 | """
76 | Mark the passage of time and decay the current rate accordingly.
77 | """
78 | prev = self._last_tick
79 | now = time()
80 | interval = now - prev
81 |
82 | instant_rate = self._uncounted / interval
83 | self._uncounted = 0
84 |
85 | if self.initialized:
86 | self._rate += (self._alpha(interval) * (instant_rate - self._rate))
87 | else:
88 | self._rate = instant_rate
89 | self.initialized = True
90 |
91 | self._last_tick = now
92 |
93 | def get_rate(self):
94 | """
95 | Returns the rate in counts per second. Calls L{EWMA.tick} when the
96 | elapsed time is greater than L{EWMA.INTERVAL}.
97 |
98 | @rtype: C{float}
99 | @return: the rate
100 | """
101 | if time() - self._last_tick >= self._interval:
102 | self.tick()
103 | return self._rate
104 |
105 | def _alpha(self, interval):
106 | """
107 | Calculate the alpha based on the time since the last tick. This is
108 | necessary because a single threaded Python program loses precision
109 | under high load, so we can't assume a consistant I{EWMA._interval}.
110 |
111 | @type interval: C{float}
112 | @param interval: the interval we use to calculate the alpha
113 | """
114 | return 1 - exp(-interval / self._period)
115 |
--------------------------------------------------------------------------------
/yunomi/stats/exp_decay_sample.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from math import exp
4 | from time import time
5 | from random import random
6 |
7 | from yunomi.stats.snapshot import Snapshot
8 |
9 |
10 | class ExponentiallyDecayingSample(object):
11 | """
12 | An exponentially-decaying random sample of longs. Uses Cormode et al's
13 | forward-decaying priority reservoir sampling method to produce a
14 | statistically representative sample, exponentially biased towards newer
15 | entries.
16 |
17 | @see:
18 | Cormode et al. Forward Decay: A Practical Time Decay Model for
19 | Streaming Systems. ICDE '09: Proceedings of the 2009 IEEE
20 | International Conference on Data Engineering (2009)
21 | """
22 | RESCALE_THRESHOLD = 3600
23 | count = 0
24 | values = {}
25 | next_scale_time = 0
26 |
27 | def __init__(self, reservoir_size, alpha, clock=time):
28 | """
29 | Creates a new L{ExponentiallyDecayingSample}.
30 |
31 | @type reservoir_size: C{int}
32 | @param reservoir_size: the number of samples to keep in the sampling
33 | reservoir
34 | @type alpha: C{float}
35 | @param alpha: the exponential decay factor; the higher this is, the more
36 | biased the sample will be towards newer values
37 | @type clock: C{function}
38 | @param clock: the function used to return the current time, default to
39 | seconds since the epoch; to be used with other time
40 | units, or with the twisted clock for our testing purposes
41 | """
42 | self.reservoir_size = reservoir_size
43 | self.alpha = alpha
44 | self.clock = clock
45 | self.clear()
46 |
47 | def clear(self):
48 | """
49 | Clears the values in the sample and resets the clock.
50 | """
51 | self.count = 0
52 | self.values = {}
53 | self.start_time = self.clock()
54 | self.next_scale_time = self.clock() + self.RESCALE_THRESHOLD
55 |
56 | def size(self):
57 | """
58 | Returns the size of the exponentially decaying sample. The size does not
59 | increase if the I{count} exceeds the I{reservoir_size}. Instead, we
60 | wait until it is time for the sample rescale.
61 |
62 | @rtype: C{int}
63 | @return: the size of the sample
64 | """
65 | return min(self.reservoir_size, self.count)
66 |
67 | def update(self, value, timestamp=None):
68 | """
69 | Adds an old value with a fixed timestamp to the sample.
70 |
71 | @type value: C{int} or C{float}
72 | @param value: the value to be added
73 | @type timestamp: C{int}
74 | @param timestamp: the epoch timestamp of I{value} in seconds
75 | """
76 | if not timestamp:
77 | timestamp = self.clock()
78 | self._rescale_if_needed()
79 | priority = self._weight(timestamp - self.start_time) / random()
80 | self.count += 1
81 |
82 | if self.count <= self.reservoir_size:
83 | self.values[priority] = value
84 | else:
85 | first = min(self.values)
86 | if first < priority:
87 | if priority not in self.values:
88 | self.values[priority] = value
89 | while first not in self.values:
90 | first = min(self.values)
91 | del self.values[first]
92 |
93 | def _rescale_if_needed(self):
94 | """
95 | Checks the current time and rescales the sample if it time to do so.
96 | """
97 | now = self.clock()
98 | next_ = self.next_scale_time
99 | if now >= next_:
100 | self._rescale(now, next_)
101 |
102 | def get_snapshot(self):
103 | """
104 | Creates a statistical snapshot from the current set of values.
105 | """
106 | return Snapshot(self.values.values())
107 |
108 | def _weight(self, t):
109 | """
110 | Assigns a weight based on a specific timer interval, used to calculate
111 | priority for each value.
112 | """
113 | return exp(self.alpha * t)
114 |
115 | def _rescale(self, now, next_):
116 | """
117 | Rescales the I{values}, assigning new priorities for each value.
118 |
119 | @type now: C{int}
120 | @param now: the time right now
121 | @type next_: C{int}
122 | @param next_: the scheduled time for the next rescale
123 | """
124 | if self.next_scale_time == next_:
125 | self.next_scale_time = now + self.RESCALE_THRESHOLD
126 | old_start_time = self.start_time
127 | self.start_time = self.clock()
128 |
129 | for key in sorted(self.values.keys()):
130 | value = self.values[key]
131 | del self.values[key]
132 | self.values[key * exp(-self.alpha * (self.start_time - old_start_time))] = value
133 |
134 | self.count = len(self.values)
135 |
--------------------------------------------------------------------------------
/yunomi/stats/snapshot.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from math import floor
4 |
5 |
6 | class Snapshot(object):
7 | """
8 | A statistical snapshot of a set of values.
9 | """
10 | MEDIAN_Q = 0.5
11 | P75_Q = 0.75
12 | P95_Q = 0.95
13 | P98_Q = .98
14 | P99_Q = .99
15 | P999_Q = .999
16 |
17 | def __init__(self, values):
18 | """
19 | Create a new L{Snapshot} with the given values.
20 |
21 | @type values: C{dict}
22 | @param values: an unordered set of values in the sample
23 | """
24 | self.values = list(values)
25 | self.values.sort()
26 |
27 | def get_value(self, quantile):
28 | """
29 | Returns the value at the given quantile.
30 |
31 | @type quantile: C{float}
32 | @param quantile: a given quantile in M{[0...1]}
33 |
34 | @rtype: C{int} or C{float}
35 | @return: the value in the distribution at the specified I{quantile}
36 | """
37 | assert quantile >= 0.0 and quantile <= 1.0,\
38 | "{0} is not in [0...1]".format(quantile)
39 | if len(self.values) == 0:
40 | return 0.0
41 |
42 | pos = quantile * (len(self.values) + 1)
43 |
44 | if pos < 1:
45 | return self.values[0]
46 | if pos >= len(self.values):
47 | return self.values[len(self.values) -1]
48 |
49 | lower = self.values[int(pos) - 1]
50 | upper = self.values[int(pos)]
51 | return lower + (pos - floor(pos)) * (upper - lower)
52 |
53 | def size(self):
54 | """
55 | Return the size of the given distribution.
56 |
57 | @rtype: C{int}
58 | @return: the size of the given distribution
59 | """
60 | return len(self.values)
61 |
62 | def get_median(self):
63 | """
64 | Return the median of the given distribution.
65 |
66 | @rtype: C{int}
67 | @return: the median
68 | """
69 | return self.get_value(self.MEDIAN_Q)
70 |
71 | def get_75th_percentile(self):
72 | """
73 | Return the 75th percentile value of the given distribution.
74 |
75 | @rtype: C{int}
76 | @return: the 99.9th percentile value
77 | """
78 | return self.get_value(self.P75_Q)
79 |
80 | def get_98th_percentile(self):
81 | """
82 | Return the 98th percentile value of the given distribution.
83 |
84 | @rtype: C{int}
85 | @return: the 98th percentile value
86 | """
87 | return self.get_value(self.P98_Q)
88 |
89 | def get_99th_percentile(self):
90 | """
91 | Return the 99th percentile value of the given distribution.
92 |
93 | @rtype: C{int}
94 | @return: the 99th percentile value
95 | """
96 | return self.get_value(self.P99_Q)
97 |
98 | def get_999th_percentile(self):
99 | """
100 | Return the 99.9th percentile value of the given distribution.
101 |
102 | @rtype: C{int}
103 | @return: the 99.9th percentile value
104 | """
105 | return self.get_value(self.P999_Q)
106 |
107 | def get_values(self):
108 | """
109 | Returns a copy of the current distribution of values
110 |
111 | @rtype: C{list}
112 | @return: a copy of the list of values
113 | """
114 | return self.values[:]
115 |
116 | def dump(output):
117 | """
118 | Write all the values to a file
119 |
120 | @todo: actually test this to see if it works...
121 | """
122 | assert type(output) == file, "Argument must be of 'file' type"
123 |
124 | for value in self.values:
125 | output.write("{0}\n".format(value))
126 | output.close()
127 |
--------------------------------------------------------------------------------
/yunomi/stats/uniform_sample.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from random import randint
4 |
5 | from yunomi.compat import xrange
6 | from yunomi.stats.snapshot import Snapshot
7 |
8 |
9 | class UniformSample(object):
10 | """
11 | A random sample of a stream of {@code long}s. Uses Vitter's Algorithm R to
12 | produce a statistically representative sample.
13 |
14 | @see: Random Sampling with a Reservoir
15 | """
16 | BITS_PER_LONG = 63
17 | values = []
18 |
19 | def __init__(self, reservoir_size):
20 | """
21 | Create a new L{UniformSample}.
22 |
23 | @type reservoir_size: C{int}
24 | @param reservoir_size: the number of params to keep in the sampling reservoir
25 | """
26 | self.values = [0 for x in xrange(reservoir_size)]
27 | self.clear()
28 |
29 | def clear(self):
30 | """
31 | Clears the sample, setting all values to zero.
32 | """
33 | self.values = [0 for x in xrange(len(self.values))]
34 | self.count = 0
35 |
36 | def size(self):
37 | """
38 | Returns the size of the uniform sample. The size will never be bigger
39 | than the reservoir_size (ie. the size of the list of values).
40 |
41 | @rtype: C{int}
42 | @return: the size of the sample
43 | """
44 |
45 | if self.count > len(self.values):
46 | return len(self.values)
47 | return self.count
48 |
49 | def update(self, value):
50 | """
51 | Updates the I{self.values} at a random index with the given value.
52 |
53 | @type value: C{int} or C{float}
54 | @param value: the new value to be added
55 | """
56 | self.count += 1
57 | if self.count <= len(self.values):
58 | self.values[self.count - 1] = value
59 | else:
60 | r = UniformSample.next_long(self.count)
61 | if r < len(self.values):
62 | self.values[r] = value
63 |
64 | @classmethod
65 | def next_long(klass, n):
66 | """
67 | Randomly assigns a new number in [0...n]. Used to randomly update an
68 | index in I{self.values} with a new value.
69 | """
70 | return randint(0, n-1)
71 |
72 | def get_snapshot(self):
73 | """
74 | Creates a statistical snapshot from the current set of values.
75 | """
76 | copy = []
77 | for i in xrange(self.size()):
78 | copy.append(self.values[i])
79 | return Snapshot(copy)
80 |
--------------------------------------------------------------------------------
/yunomi/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreid/yunomi/1aa3f166843613331b38e231264ffc3ac40e8094/yunomi/tests/__init__.py
--------------------------------------------------------------------------------
/yunomi/tests/test_EWMA.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | import mock
4 | from unittest2 import TestCase
5 |
6 | from yunomi.compat import xrange
7 | from yunomi.stats.ewma import EWMA
8 |
9 |
10 | class EWMATests(TestCase):
11 |
12 | def elapse_minute(self, time_mock):
13 | for i in xrange(0, 12):
14 | time_mock.return_value += 5
15 | self.ewma.tick()
16 |
17 | @mock.patch("yunomi.stats.ewma.time")
18 | def test_one_minute_EWMA_five_sec_tick(self, time_mock):
19 | time_mock.return_value = 0.0
20 | self.ewma = EWMA.one_minute_EWMA()
21 |
22 | self.ewma.update(3)
23 | time_mock.return_value += 5
24 | self.ewma.tick()
25 |
26 | for expected_rate in [0.6, 0.22072766, 0.08120117, 0.02987224,
27 | 0.01098938, 0.00404277, 0.00148725,
28 | 0.00054713, 0.00020128, 0.00007405]:
29 | self.assertAlmostEqual(self.ewma.get_rate(), expected_rate)
30 | self.elapse_minute(time_mock)
31 |
32 | @mock.patch("yunomi.stats.ewma.time")
33 | def test_five_minute_EWMA_five_sec_tick(self, time_mock):
34 | time_mock.return_value = 0.0
35 | self.ewma = EWMA.five_minute_EWMA()
36 |
37 | self.ewma.update(3)
38 | time_mock.return_value += 5
39 | self.ewma.tick()
40 |
41 | for expected_rate in [0.6, 0.49123845, 0.40219203, 0.32928698,
42 | 0.26959738, 0.22072766, 0.18071653,
43 | 0.14795818, 0.12113791, 0.09917933]:
44 | self.assertAlmostEqual(self.ewma.get_rate(), expected_rate)
45 | self.elapse_minute(time_mock)
46 |
47 | @mock.patch("yunomi.stats.ewma.time")
48 | def test_fifteen_minute_EWMA_five_sec_tick(self, time_mock):
49 | time_mock.return_value = 0.0
50 | self.ewma = EWMA.fifteen_minute_EWMA()
51 |
52 | self.ewma.update(3)
53 | time_mock.return_value += 5
54 | self.ewma.tick()
55 |
56 | for expected_rate in [0.6, 0.56130419, 0.52510399, 0.49123845,
57 | 0.45955700, 0.42991879, 0.40219203,
58 | 0.37625345, 0.35198773, 0.32928698]:
59 | self.assertAlmostEqual(self.ewma.get_rate(), expected_rate)
60 | self.elapse_minute(time_mock)
61 |
62 | @mock.patch("yunomi.stats.ewma.time")
63 | def test_one_minute_EWMA_one_minute_tick(self, time_mock):
64 | time_mock.return_value = 0.0
65 | self.ewma = EWMA.one_minute_EWMA()
66 |
67 | self.ewma.update(3)
68 | time_mock.return_value += 5
69 | self.ewma.tick()
70 |
71 | for expected_rate in [0.6, 0.22072766, 0.08120117, 0.02987224,
72 | 0.01098938, 0.00404277, 0.00148725,
73 | 0.00054713, 0.00020128, 0.00007405]:
74 | self.assertAlmostEqual(self.ewma.get_rate(), expected_rate)
75 | time_mock.return_value += 60
76 |
77 | @mock.patch("yunomi.stats.ewma.time")
78 | def test_five_minute_EWMA_one_minute_tick(self, time_mock):
79 | time_mock.return_value = 0.0
80 | self.ewma = EWMA.five_minute_EWMA()
81 |
82 | self.ewma.update(3)
83 | time_mock.return_value += 5
84 | self.ewma.tick()
85 |
86 | for expected_rate in [0.6, 0.49123845, 0.40219203, 0.32928698,
87 | 0.26959738, 0.22072766, 0.18071653,
88 | 0.14795818, 0.12113791, 0.09917933]:
89 | self.assertAlmostEqual(self.ewma.get_rate(), expected_rate)
90 | time_mock.return_value += 60
91 |
92 | @mock.patch("yunomi.stats.ewma.time")
93 | def test_fifteen_minute_EWMA_one_minute_tick(self, time_mock):
94 | time_mock.return_value = 0.0
95 | self.ewma = EWMA.fifteen_minute_EWMA()
96 |
97 | self.ewma.update(3)
98 | time_mock.return_value += 5
99 | self.ewma.tick()
100 |
101 | for expected_rate in [0.6, 0.56130419, 0.52510399, 0.49123845,
102 | 0.45955700, 0.42991879, 0.40219203,
103 | 0.37625345, 0.35198773, 0.32928698]:
104 | self.assertAlmostEqual(self.ewma.get_rate(), expected_rate)
105 | time_mock.return_value += 60
106 |
--------------------------------------------------------------------------------
/yunomi/tests/test_counter.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from unittest2 import TestCase
4 |
5 | from yunomi.core.counter import Counter
6 |
7 |
8 | class CounterTests(TestCase):
9 | _counter = Counter()
10 |
11 | def test_starts_at_zero(self):
12 | self.assertEqual(self._counter.get_count(), 0)
13 |
14 | def test_increments_by_one(self):
15 | self._counter.inc()
16 | self.assertEqual(self._counter.get_count(), 1)
17 | self._counter.clear()
18 |
19 | def test_increments_by_arbitrary_delta(self):
20 | self._counter.inc(12)
21 | self.assertEqual(self._counter.get_count(), 12)
22 | self._counter.clear()
23 |
24 | def test_decrements_by_one(self):
25 | self._counter.dec()
26 | self.assertEqual(self._counter.get_count(), -1)
27 | self._counter.clear()
28 |
29 | def test_decrements_by_arbitrary_delta(self):
30 | self._counter.dec(12)
31 | self.assertEqual(self._counter.get_count(), -12)
32 | self._counter.clear()
33 |
34 | def test_is_zero_after_being_cleared(self):
35 | self._counter.clear()
36 | self.assertEqual(self._counter.get_count(), 0)
37 |
--------------------------------------------------------------------------------
/yunomi/tests/test_exp_decay_sample_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from unittest2 import TestCase
4 |
5 | from yunomi.compat import xrange
6 | from yunomi.stats.exp_decay_sample import ExponentiallyDecayingSample
7 | from yunomi.tests.util import Clock
8 |
9 |
10 | class ExponentiallyDecayingSampleTests(TestCase):
11 |
12 | def test_a_sample_of_100_out_of_1000_elements(self):
13 | sample = ExponentiallyDecayingSample(100, 0.99)
14 | for i in xrange(1000):
15 | sample.update(i)
16 | snapshot = sample.get_snapshot()
17 |
18 | self.assertEqual(sample.size(), 100)
19 | self.assertEqual(snapshot.size(), 100)
20 |
21 | for i in snapshot.get_values():
22 | self.assertTrue(i < 1000 and i >= 0)
23 |
24 | def test_a_sample_of_100_out_of_10_elements(self):
25 | sample = ExponentiallyDecayingSample(100, 0.99)
26 | for i in xrange(10):
27 | sample.update(i)
28 | snapshot = sample.get_snapshot()
29 |
30 | self.assertEqual(sample.size(), 10)
31 | self.assertEqual(snapshot.size(), 10)
32 | self.assertAlmostEqual(snapshot.get_median(), 4.5)
33 |
34 | for i in snapshot.get_values():
35 | self.assertTrue(i < 10 and i >= 0)
36 |
37 | def test_a_heavily_biased_sample_of_100_out_of_1000_elements(self):
38 | sample = ExponentiallyDecayingSample(1000, 0.01)
39 | for i in xrange(100):
40 | sample.update(i)
41 | snapshot = sample.get_snapshot()
42 |
43 | self.assertEqual(sample.size(), 100)
44 | self.assertEqual(snapshot.size(), 100)
45 |
46 | for i in snapshot.get_values():
47 | self.assertTrue(i < 100 and i >= 0)
48 |
49 | def test_long_period_of_inactivity_should_not_corrupt_sampling_state(self):
50 | twisted_clock = Clock()
51 | sample = ExponentiallyDecayingSample(10, 0.015, twisted_clock.seconds)
52 | for i in xrange(1000):
53 | sample.update(1000 + i)
54 | twisted_clock.advance(0.1)
55 |
56 | self.assertTrue(sample.get_snapshot().size() == 10)
57 | self._assert_all_values_between(sample, 1000, 2000)
58 |
59 | twisted_clock.advance(15*3600)
60 | sample.update(2000)
61 | self.assertTrue(sample.get_snapshot().size() == 2)
62 | self._assert_all_values_between(sample, 1000, 3000)
63 |
64 | for i in xrange(1000):
65 | sample.update(3000 + i)
66 | twisted_clock.advance(0.1)
67 |
68 | self.assertTrue(sample.get_snapshot().size() == 10)
69 | self._assert_all_values_between(sample, 3000, 4000)
70 |
71 | def _assert_all_values_between(self, sample, lower, upper):
72 | for value in sample.get_snapshot().get_values():
73 | self.assertTrue(value >= lower and value < upper)
74 |
--------------------------------------------------------------------------------
/yunomi/tests/test_histogram.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from unittest2 import TestCase
4 |
5 | from yunomi.compat import xrange
6 | from yunomi.core.histogram import Histogram
7 |
8 |
9 | class HistogramTests(TestCase):
10 |
11 | def setUp(self):
12 | self.histogram_b = Histogram.get_biased()
13 | self.histogram_u = Histogram.get_uniform()
14 |
15 | def test_unique_biased_histogram(self):
16 | new_histogram = Histogram.get_biased()
17 | self.assertIsNot(new_histogram.sample, self.histogram_b.sample)
18 |
19 | def test_unique_uniform_histogram(self):
20 | new_histogram = Histogram.get_uniform()
21 | self.assertIsNot(new_histogram.sample, self.histogram_u.sample)
22 |
23 | def test_empty_histogram(self):
24 | for histogram in self.histogram_b, self.histogram_u:
25 | histogram.clear()
26 | self.assertEqual(histogram.get_count(), 0)
27 | self.assertAlmostEqual(histogram.get_max(), 0)
28 | self.assertAlmostEqual(histogram.get_min(), 0)
29 | self.assertAlmostEqual(histogram.get_mean(), 0)
30 | self.assertAlmostEqual(histogram.get_std_dev(), 0)
31 | self.assertAlmostEqual(histogram.get_sum(), 0)
32 |
33 | snapshot = histogram.get_snapshot()
34 | self.assertAlmostEqual(snapshot.get_median(), 0)
35 | self.assertAlmostEqual(snapshot.get_75th_percentile(), 0)
36 | self.assertAlmostEqual(snapshot.get_99th_percentile(), 0)
37 | self.assertAlmostEqual(snapshot.size(), 0)
38 |
39 | def test_histogram_with_1000_elements(self):
40 | for histogram in self.histogram_b, self.histogram_u:
41 | histogram.clear()
42 | for i in xrange(1, 1001):
43 | histogram.update(i)
44 |
45 | self.assertEqual(histogram.get_count(), 1000)
46 | self.assertAlmostEqual(histogram.get_max(), 1000)
47 | self.assertAlmostEqual(histogram.get_min(), 1)
48 | self.assertAlmostEqual(histogram.get_mean(), 500.5)
49 | self.assertAlmostEqual(histogram.get_std_dev(), 288.8194360957494, places=3)
50 | self.assertAlmostEqual(histogram.get_sum(), 500500)
51 |
52 | snapshot = histogram.get_snapshot()
53 | self.assertAlmostEqual(snapshot.get_median(), 500.5)
54 | self.assertAlmostEqual(snapshot.get_75th_percentile(), 750.75)
55 | self.assertAlmostEqual(snapshot.get_99th_percentile(), 990.99)
56 | self.assertAlmostEqual(snapshot.size(), 1000)
57 |
--------------------------------------------------------------------------------
/yunomi/tests/test_meter.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | import mock
4 | from unittest2 import TestCase
5 |
6 | from yunomi.compat import xrange
7 | from yunomi.core.meter import Meter
8 |
9 |
10 | class MeterTests(TestCase):
11 |
12 | def test_a_blankmeter(self):
13 | self.meter = Meter("test")
14 | self.assertEqual(self.meter.get_count(), 0)
15 | self.assertAlmostEqual(self.meter.get_mean_rate(), 0.0)
16 |
17 | def test_meter_with_three_events(self):
18 | self.meter = Meter("test")
19 | self.meter.mark(3)
20 | self.assertEqual(self.meter.get_count(), 3)
21 |
22 | @mock.patch("yunomi.core.meter.time")
23 | def test_mean_rate_one_per_second(self, time_mock):
24 | time_mock.return_value = 0.0
25 | self.meter = Meter("test")
26 | for i in xrange(10):
27 | self.meter.mark()
28 | time_mock.return_value += 1
29 |
30 | self.meter._tick()
31 | self.assertAlmostEqual(self.meter.get_mean_rate(), 1)
32 |
33 | @mock.patch("yunomi.stats.ewma.time")
34 | def test_meter_EWMA_rates(self, time_mock):
35 | time_mock.return_value = 0.0
36 | self.meter = Meter("test")
37 | self.meter.mark(3)
38 | time_mock.return_value += 5
39 |
40 | for one, five, fifteen in [(0.6, 0.6, 0.6),
41 | (0.22072766, 0.49123845, 0.56130419),
42 | (0.08120117, 0.40219203, 0.52510399),
43 | (0.02987224, 0.32928698, 0.49123845),
44 | (0.01098938, 0.26959738, 0.45955700),
45 | (0.00404277, 0.22072766, 0.42991879),
46 | (0.00148725, 0.18071653, 0.40219203),
47 | (0.00054713, 0.14795818, 0.37625345),
48 | (0.00020128, 0.12113791, 0.35198773),
49 | (0.00007405, 0.09917933, 0.32928698)]:
50 | self.assertAlmostEqual(self.meter.get_one_minute_rate(), one)
51 | self.assertAlmostEqual(self.meter.get_five_minute_rate(), five)
52 | self.assertAlmostEqual(self.meter.get_fifteen_minute_rate(), fifteen)
53 | time_mock.return_value += 60
54 |
--------------------------------------------------------------------------------
/yunomi/tests/test_metrics_registry.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | import mock
4 | from unittest2 import TestCase
5 |
6 | from yunomi.compat import xrange
7 | from yunomi.core.metrics_registry import (MetricsRegistry, counter, histogram,
8 | meter, timer, count_calls,
9 | meter_calls, hist_calls, time_calls)
10 | from yunomi.tests.util import Clock
11 |
12 |
13 | class MetricsRegistryTests(TestCase):
14 |
15 | def setUp(self):
16 | self.twisted_clock = Clock()
17 | self.registry = MetricsRegistry(clock=self.twisted_clock.seconds)
18 |
19 | def test_empty_registry(self):
20 | self.assertEqual(len(self.registry.dump_metrics()), 0)
21 |
22 | def test_getters_create_metrics(self):
23 | self.registry.counter("counter")
24 | self.registry.histogram("histogram")
25 | self.registry.meter("meter")
26 | self.registry.timer("timer")
27 |
28 | dump = self.registry.dump_metrics()
29 |
30 | self.assertEqual(len(dump), 25)
31 | metric_names = ("counter_count", "histogram_avg", "histogram_max",
32 | "histogram_min", "histogram_std_dev",
33 | "histogram_75_percentile", "histogram_98_percentile",
34 | "histogram_99_percentile", "histogram_999_percentile",
35 | "meter_15m_rate", "meter_5m_rate", "meter_1m_rate",
36 | "meter_mean_rate", "timer_avg", "timer_max",
37 | "timer_min", "timer_std_dev", "timer_75_percentile",
38 | "timer_98_percentile", "timer_99_percentile",
39 | "timer_999_percentile", "timer_15m_rate",
40 | "timer_5m_rate", "timer_1m_rate", "timer_mean_rate")
41 | for stat in dump:
42 | self.assertTrue(stat["name"] in metric_names)
43 | self.assertEqual(stat["value"], 0)
44 |
45 | def test_count_calls_decorator(self):
46 | @count_calls
47 | def test():
48 | pass
49 |
50 | for i in xrange(10):
51 | test()
52 | self.assertEqual(counter("test_calls").get_count(), 10)
53 |
54 | @mock.patch("yunomi.core.meter.time")
55 | def test_meter_calls_decorator(self, time_mock):
56 | time_mock.return_value = 0
57 | @meter_calls
58 | def test():
59 | pass
60 |
61 | for i in xrange(10):
62 | test()
63 | time_mock.return_value = 10
64 | self.assertAlmostEqual(meter("test_calls").get_mean_rate(), 1.0)
65 |
66 |
67 | def test_hist_calls_decorator(self):
68 | @hist_calls
69 | def test(n):
70 | return n
71 |
72 | for i in xrange(1, 11):
73 | test(i)
74 |
75 | _histogram = histogram("test_calls")
76 | snapshot = _histogram.get_snapshot()
77 | self.assertAlmostEqual(_histogram.get_mean(), 5.5)
78 | self.assertEqual(_histogram.get_max(), 10)
79 | self.assertEqual(_histogram.get_min(), 1)
80 | self.assertAlmostEqual(_histogram.get_std_dev(), 3.02765, places=5)
81 | self.assertAlmostEqual(_histogram.get_variance(), 9.16667, places=5)
82 | self.assertAlmostEqual(snapshot.get_75th_percentile(), 8.25)
83 | self.assertAlmostEqual(snapshot.get_98th_percentile(), 10.0)
84 | self.assertAlmostEqual(snapshot.get_99th_percentile(), 10.0)
85 | self.assertAlmostEqual(snapshot.get_999th_percentile(), 10.0)
86 |
87 | @mock.patch("yunomi.core.metrics_registry.time")
88 | def test_time_calls_decorator(self, time_mock):
89 | time_mock.return_value = 0.0
90 | @time_calls
91 | def test():
92 | time_mock.return_value += 1.0
93 |
94 | for i in xrange(10):
95 | test()
96 | _timer = timer("test_calls")
97 | snapshot = _timer.get_snapshot()
98 | self.assertEqual(_timer.get_count(), 10)
99 | self.assertEqual(_timer.get_max(), 1)
100 | self.assertEqual(_timer.get_min(), 1)
101 | self.assertAlmostEqual(_timer.get_std_dev(), 0)
102 | self.assertAlmostEqual(snapshot.get_75th_percentile(), 1.0)
103 | self.assertAlmostEqual(snapshot.get_98th_percentile(), 1.0)
104 | self.assertAlmostEqual(snapshot.get_99th_percentile(), 1.0)
105 | self.assertAlmostEqual(snapshot.get_999th_percentile(), 1.0)
106 |
107 | def test_count_calls_decorator_returns_original_return_value(self):
108 | @count_calls
109 | def test():
110 | return 1
111 | self.assertEqual(test(), 1)
112 |
113 | def test_meter_calls_decorator_returns_original_return_value(self):
114 | @meter_calls
115 | def test():
116 | return 1
117 | self.assertEqual(test(), 1)
118 |
119 | def test_hist_calls_decorator_returns_original_return_value(self):
120 | @hist_calls
121 | def test():
122 | return 1
123 | self.assertEqual(test(), 1)
124 |
125 | def test_time_calls_decorator_returns_original_return_value(self):
126 | @time_calls
127 | def test():
128 | return 1
129 | self.assertEqual(test(), 1)
130 |
131 | def test_count_calls_decorator_keeps_function_name(self):
132 | @count_calls
133 | def test():
134 | pass
135 | self.assertEqual(test.__name__, 'test')
136 |
137 | def test_meter_calls_decorator_keeps_function_name(self):
138 | @meter_calls
139 | def test():
140 | pass
141 | self.assertEqual(test.__name__, 'test')
142 |
143 | def test_hist_calls_decorator_keeps_function_name(self):
144 | @hist_calls
145 | def test():
146 | pass
147 | self.assertEqual(test.__name__, 'test')
148 |
149 | def test_time_calls_decorator_keeps_function_name(self):
150 | @time_calls
151 | def test():
152 | pass
153 | self.assertEqual(test.__name__, 'test')
154 |
155 | def test_count_calls_decorator_propagates_errors(self):
156 | @count_calls
157 | def test():
158 | raise Exception('what')
159 | self.assertRaises(Exception, test)
160 |
161 | def test_meter_calls_decorator_propagates_errors(self):
162 | @meter_calls
163 | def test():
164 | raise Exception('what')
165 | self.assertRaises(Exception, test)
166 |
167 | def test_hist_calls_decorator_propagates_errors(self):
168 | @hist_calls
169 | def test():
170 | raise Exception('what')
171 | self.assertRaises(Exception, test)
172 |
173 | def test_time_calls_decorator_propagates_errors(self):
174 | @time_calls
175 | def test():
176 | raise Exception('what')
177 | self.assertRaises(Exception, test)
178 |
--------------------------------------------------------------------------------
/yunomi/tests/test_snapshot.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from unittest2 import TestCase
4 |
5 | from yunomi.stats.snapshot import Snapshot
6 |
7 |
8 | class SnapshotTests(TestCase):
9 | def setUp(self):
10 | self.snapshot = Snapshot([5, 1, 2, 3, 4])
11 |
12 | def test_small_quantiles_are_the_first_value(self):
13 | self.assertAlmostEqual(self.snapshot.get_value(0.0), 1)
14 |
15 | def test_big_quantiles_are_the_last_value(self):
16 | self.assertAlmostEqual(self.snapshot.get_value(1.0), 5)
17 |
18 | def test_has_a_median(self):
19 | self.assertAlmostEqual(self.snapshot.get_median(), 3)
20 |
21 | def test_percentiles(self):
22 | percentiles = [(4.5, self.snapshot.get_75th_percentile),
23 | (5, self.snapshot.get_98th_percentile),
24 | (5, self.snapshot.get_99th_percentile),
25 | (5, self.snapshot.get_999th_percentile)]
26 |
27 | for val, func in percentiles:
28 | self.assertAlmostEqual(func(), val)
29 |
30 | def test_has_values(self):
31 | self.assertEquals(self.snapshot.get_values(), [1, 2, 3, 4, 5])
32 |
33 | def test_has_a_size(self):
34 | self.assertEquals(self.snapshot.size(), 5)
35 |
--------------------------------------------------------------------------------
/yunomi/tests/test_timer.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from unittest2 import TestCase
4 |
5 | from yunomi.core.timer import Timer
6 |
7 |
8 | class TimerTests(TestCase):
9 |
10 | def setUp(self):
11 | self.timer = Timer()
12 |
13 | def test_blank_timer(self):
14 | self.assertEqual(self.timer.get_count(), 0)
15 | self.assertAlmostEqual(self.timer.get_max(), 0.0)
16 | self.assertAlmostEqual(self.timer.get_min(), 0.0)
17 | self.assertAlmostEqual(self.timer.get_mean(), 0.0)
18 | self.assertAlmostEqual(self.timer.get_std_dev(), 0.0)
19 |
20 | snapshot = self.timer.get_snapshot()
21 | self.assertAlmostEqual(snapshot.get_median(), 0.0)
22 | self.assertAlmostEqual(snapshot.get_75th_percentile(), 0.0)
23 | self.assertAlmostEqual(snapshot.get_99th_percentile(), 0.0)
24 | self.assertEqual(self.timer.get_snapshot().size(), 0)
25 |
26 | self.assertAlmostEqual(self.timer.get_mean_rate(), 0.0)
27 | self.assertAlmostEqual(self.timer.get_one_minute_rate(), 0.0)
28 | self.assertAlmostEqual(self.timer.get_five_minute_rate(), 0.0)
29 | self.assertAlmostEqual(self.timer.get_fifteen_minute_rate(), 0.0)
30 |
31 | def test_timing_a_series_of_events(self):
32 | self.timer = Timer()
33 | self.timer.update(10)
34 | self.timer.update(20)
35 | self.timer.update(20)
36 | self.timer.update(30)
37 | self.timer.update(40)
38 |
39 | self.assertEqual(self.timer.get_count(), 5)
40 | self.assertAlmostEqual(self.timer.get_max(), 40.0)
41 | self.assertAlmostEqual(self.timer.get_min(), 10.0)
42 | self.assertAlmostEqual(self.timer.get_mean(), 24.0)
43 | self.assertAlmostEqual(self.timer.get_std_dev(), 11.401, places=2)
44 |
45 | snapshot = self.timer.get_snapshot()
46 | self.assertAlmostEqual(snapshot.get_median(), 20.0)
47 | self.assertAlmostEqual(snapshot.get_75th_percentile(), 35.0)
48 | self.assertAlmostEqual(snapshot.get_99th_percentile(), 40.0)
49 | self.assertEqual(self.timer.get_snapshot().get_values(),
50 | [10.0, 20.0, 20.0, 30.0, 40.0])
51 |
52 | def test_timing_variant_values(self):
53 | self.timer.clear()
54 | self.timer.update(9223372036854775807)
55 | self.timer.update(0)
56 | self.assertAlmostEqual(self.timer.get_std_dev(), 6521908912666392000)
57 |
--------------------------------------------------------------------------------
/yunomi/tests/test_uniform_sample.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 | from unittest2 import TestCase
4 |
5 | from yunomi.compat import xrange
6 | from yunomi.stats.uniform_sample import UniformSample
7 |
8 |
9 | class UniformSampleTests(TestCase):
10 |
11 | def test_a_sample_of_100_out_of_1000_elements(self):
12 | sample = UniformSample(100)
13 | for i in xrange(1000):
14 | sample.update(i)
15 | snapshot = sample.get_snapshot()
16 |
17 | self.assertEqual(sample.size(), 100)
18 | self.assertEqual(snapshot.size(), 100)
19 |
20 | for i in snapshot.get_values():
21 | self.assertTrue(i < 1000 and i >= 0)
22 |
--------------------------------------------------------------------------------
/yunomi/tests/util.py:
--------------------------------------------------------------------------------
1 | from __future__ import division, absolute_import
2 |
3 |
4 | class Clock(object):
5 | """
6 | Stripped down version of C{twisted.internet.task.Clock} from Twisted 13.0.0
7 | """
8 | rightNow = 0.0
9 |
10 | def seconds(self):
11 | """
12 | Pretend to be time.time().
13 |
14 | @rtype: C{float}
15 | @return: The time which should be considered the current time.
16 | """
17 | return self.rightNow
18 |
19 | def advance(self, amount):
20 | """
21 | Move time on this clock forward by the given amount.
22 |
23 | @type amount: C{float}
24 | @param amount: The number of seconds which to advance this clock's
25 | time.
26 | """
27 | self.rightNow += amount
28 |
29 |
30 | __all__ = [
31 | Clock
32 | ]
33 |
--------------------------------------------------------------------------------