diff options
Diffstat (limited to 'library/cpp/monlib/counters')
-rw-r--r-- | library/cpp/monlib/counters/counters.cpp | 49 | ||||
-rw-r--r-- | library/cpp/monlib/counters/counters.h | 350 | ||||
-rw-r--r-- | library/cpp/monlib/counters/counters_ut.cpp | 49 | ||||
-rw-r--r-- | library/cpp/monlib/counters/histogram.cpp | 40 | ||||
-rw-r--r-- | library/cpp/monlib/counters/histogram.h | 189 | ||||
-rw-r--r-- | library/cpp/monlib/counters/histogram_ut.cpp | 47 | ||||
-rw-r--r-- | library/cpp/monlib/counters/meter.cpp | 4 | ||||
-rw-r--r-- | library/cpp/monlib/counters/meter.h | 285 | ||||
-rw-r--r-- | library/cpp/monlib/counters/meter_ut.cpp | 41 | ||||
-rw-r--r-- | library/cpp/monlib/counters/timer.h | 176 | ||||
-rw-r--r-- | library/cpp/monlib/counters/timer_ut.cpp | 81 | ||||
-rw-r--r-- | library/cpp/monlib/counters/ut/ya.make | 12 | ||||
-rw-r--r-- | library/cpp/monlib/counters/ya.make | 15 |
13 files changed, 1338 insertions, 0 deletions
diff --git a/library/cpp/monlib/counters/counters.cpp b/library/cpp/monlib/counters/counters.cpp new file mode 100644 index 0000000000..50dca4c577 --- /dev/null +++ b/library/cpp/monlib/counters/counters.cpp @@ -0,0 +1,49 @@ + +#include "counters.h" + +namespace NMonitoring { + char* PrettyNumShort(i64 val, char* buf, size_t size) { + static const char shorts[] = {' ', 'K', 'M', 'G', 'T', 'P', 'E'}; + unsigned i = 0; + i64 major = val; + i64 minor = 0; + const unsigned imax = sizeof(shorts) / sizeof(char); + for (i = 0; i < imax; i++) { + if (major >> 10 == 0) + break; + else { + minor = major - (major >> 10 << 10); + major = major >> 10; + } + } + minor = (minor * 10) >> 10; + + if (i == 0 || i >= imax) + *buf = '\0'; + else + snprintf(buf, size, "%" PRId64 ".%" PRId64 "%c", major, minor, shorts[i]); + + return buf; + } + + char* PrettyNum(i64 val, char* buf, size_t size) { + Y_ASSERT(buf); + if (size < 4) { + buf[0] = 0; + return buf; + } + PrettyNumShort(val, buf + 2, size - 3); + if (buf[2] == 0) { + *buf = '\0'; + } else { + size_t len = 2 + strnlen(buf + 2, size - 4); + Y_ASSERT(len < size); + buf[0] = ' '; + buf[1] = '('; + buf[len] = ')'; + buf[len + 1] = '\0'; + } + + return buf; + } +} diff --git a/library/cpp/monlib/counters/counters.h b/library/cpp/monlib/counters/counters.h new file mode 100644 index 0000000000..038b55f0c8 --- /dev/null +++ b/library/cpp/monlib/counters/counters.h @@ -0,0 +1,350 @@ +#pragma once + +#include <util/datetime/base.h> +#include <util/generic/algorithm.h> +#include <util/generic/list.h> +#include <util/generic/map.h> +#include <util/generic/ptr.h> +#include <util/generic/singleton.h> +#include <util/generic/vector.h> +#include <util/str_stl.h> +#include <util/stream/output.h> +#include <util/string/util.h> +#include <util/system/atomic.h> +#include <util/system/defaults.h> +#include <util/system/guard.h> +#include <util/system/sem.h> +#include <util/system/spinlock.h> + +#include <array> + +namespace NMonitoring { +#define BEGIN_OUTPUT_COUNTERS \ + void OutputImpl(IOutputStream& out) { \ + char prettyBuf[32]; +#define END_OUTPUT_COUNTERS \ + out.Flush(); \ + } + +#define OUTPUT_NAMED_COUNTER(var, name) out << name << ": \t" << var << NMonitoring::PrettyNum(var, prettyBuf, 32) << '\n' +#define OUTPUT_COUNTER(var) OUTPUT_NAMED_COUNTER(var, #var); + + char* PrettyNumShort(i64 val, char* buf, size_t size); + char* PrettyNum(i64 val, char* buf, size_t size); + + // This class is deprecated. Please consider to use + // library/cpp/monlib/metrics instead. See more info at + // https://wiki.yandex-team.ru/solomon/libs/monlib_cpp/ + class TDeprecatedCounter { + public: + using TValue = TAtomic; + using TValueBase = TAtomicBase; + + TDeprecatedCounter() + : Value() + , Derivative(false) + { + } + + TDeprecatedCounter(TValue value, bool derivative = false) + : Value(value) + , Derivative(derivative) + { + } + + bool ForDerivative() const { + return Derivative; + } + + operator TValueBase() const { + return AtomicGet(Value); + } + TValueBase Val() const { + return AtomicGet(Value); + } + + void Set(TValue val) { + AtomicSet(Value, val); + } + + TValueBase Inc() { + return AtomicIncrement(Value); + } + TValueBase Dec() { + return AtomicDecrement(Value); + } + + TValueBase Add(const TValue val) { + return AtomicAdd(Value, val); + } + TValueBase Sub(const TValue val) { + return AtomicAdd(Value, -val); + } + + // operator overloads convinient + void operator++() { + Inc(); + } + void operator++(int) { + Inc(); + } + + void operator--() { + Dec(); + } + void operator--(int) { + Dec(); + } + + void operator+=(TValue rhs) { + Add(rhs); + } + void operator-=(TValue rhs) { + Sub(rhs); + } + + TValueBase operator=(TValue rhs) { + AtomicSwap(&Value, rhs); + return rhs; + } + + bool operator!() const { + return AtomicGet(Value) == 0; + } + + TAtomic& GetAtomic() { + return Value; + } + + private: + TAtomic Value; + bool Derivative; + }; + + template <typename T> + struct TDeprecatedCountersBase { + virtual ~TDeprecatedCountersBase() { + } + + virtual void OutputImpl(IOutputStream&) = 0; + + static T& Instance() { + return *Singleton<T>(); + } + + static void Output(IOutputStream& out) { + Instance().OutputImpl(out); + } + }; + + // This class is deprecated. Please consider to use + // library/cpp/monlib/metrics instead. See more info at + // https://wiki.yandex-team.ru/solomon/libs/monlib_cpp/ + // + // Groups of G counters, defined by T type. + // Less(a,b) returns true, if a < b. + // It's threadsafe. + template <typename T, typename G, typename TL = TLess<T>> + class TDeprecatedCounterGroups { + public: + typedef TMap<T, G*> TGroups; + typedef TVector<T> TGroupsNames; + typedef THolder<TGroupsNames> TGroupsNamesPtr; + + private: + class TCollection { + struct TElement { + T* Name; + G* Counters; + + public: + static bool Compare(const TElement& a, const TElement& b) { + return Less(*(a.Name), *(b.Name)); + } + }; // TElement + private: + TArrayHolder<TElement> Elements; + size_t Size; + + public: + TCollection() + : Size(0) + { + } + + TCollection(const TCollection& collection) + : Elements(new TElement[collection.Size]) + , Size(collection.Size) + { + for (int i = 0; i < Size; ++i) { + Elements[i] = collection.Elements[i]; + } + } + + TCollection(const TCollection& collection, T* name, G* counters) + : Elements(new TElement[collection.Size + 1]) + , Size(collection.Size + 1) + { + for (size_t i = 0; i < Size - 1; ++i) { + Elements[i] = collection.Elements[i]; + } + Elements[Size - 1].Name = name; + Elements[Size - 1].Counters = counters; + for (size_t i = 1; i < Size; ++i) { + size_t j = i; + while (j > 0 && + TElement::Compare(Elements[j], Elements[j - 1])) { + std::swap(Elements[j], Elements[j - 1]); + --j; + } + } + } + + G* Find(const T& name) const { + G* result = nullptr; + if (Size == 0) { + return nullptr; + } + size_t l = 0; + size_t r = Size - 1; + while (l < r) { + size_t m = (l + r) / 2; + if (Less(*(Elements[m].Name), name)) { + l = m + 1; + } else { + r = m; + } + } + if (!Less(*(Elements[l].Name), name) && !Less(name, *(Elements[l].Name))) { + result = Elements[l].Counters; + } + return result; + } + + void Free() { + for (size_t i = 0; i < Size; ++i) { + T* name = Elements[i].Name; + G* counters = Elements[i].Counters; + Elements[i].Name = nullptr; + Elements[i].Counters = nullptr; + delete name; + delete counters; + } + Size = 0; + } + + TGroupsNamesPtr GetNames() const { + TGroupsNamesPtr result(new TGroupsNames()); + for (size_t i = 0; i < Size; ++i) { + result->push_back(*(Elements[i].Name)); + } + return result; + } + }; // TCollection + struct TOldGroup { + TCollection* Collection; + ui64 Time; + }; + + private: + TCollection* Groups; + TList<TOldGroup> OldGroups; + TSpinLock AddMutex; + + ui64 Timeout; + + static TL Less; + + private: + G* Add(const T& name) { + TGuard<TSpinLock> guard(AddMutex); + G* result = Groups->Find(name); + if (result == nullptr) { + T* newName = new T(name); + G* newCounters = new G(); + TCollection* newGroups = + new TCollection(*Groups, newName, newCounters); + ui64 now = ::Now().MicroSeconds(); + TOldGroup group; + group.Collection = Groups; + group.Time = now; + OldGroups.push_back(group); + for (ui32 i = 0; i < 5; ++i) { + if (OldGroups.front().Time + Timeout < now) { + delete OldGroups.front().Collection; + OldGroups.front().Collection = nullptr; + OldGroups.pop_front(); + } else { + break; + } + } + Groups = newGroups; + result = Groups->Find(name); + } + return result; + } + + public: + TDeprecatedCounterGroups(ui64 timeout = 5 * 1000000L) { + Groups = new TCollection(); + Timeout = timeout; + } + + virtual ~TDeprecatedCounterGroups() { + TGuard<TSpinLock> guard(AddMutex); + Groups->Free(); + delete Groups; + Groups = nullptr; + typename TList<TOldGroup>::iterator i; + for (i = OldGroups.begin(); i != OldGroups.end(); ++i) { + delete i->Collection; + i->Collection = nullptr; + } + OldGroups.clear(); + } + + bool Has(const T& name) const { + TCollection* groups = Groups; + return groups->Find(name) != nullptr; + } + + G* Find(const T& name) const { + TCollection* groups = Groups; + return groups->Find(name); + } + + // Get group with the name, if it exists. + // If there is no group with the name, add new group. + G& Get(const T& name) { + G* result = Find(name); + if (result == nullptr) { + result = Add(name); + Y_ASSERT(result != nullptr); + } + return *result; + } + + // Get copy of groups names array. + TGroupsNamesPtr GetGroupsNames() const { + TCollection* groups = Groups; + TGroupsNamesPtr result = groups->GetNames(); + return result; + } + }; // TDeprecatedCounterGroups + + template <typename T, typename G, typename TL> + TL TDeprecatedCounterGroups<T, G, TL>::Less; +} + +static inline IOutputStream& operator<<(IOutputStream& o, const NMonitoring::TDeprecatedCounter& rhs) { + return o << rhs.Val(); +} + +template <size_t N> +static inline IOutputStream& operator<<(IOutputStream& o, const std::array<NMonitoring::TDeprecatedCounter, N>& rhs) { + for (typename std::array<NMonitoring::TDeprecatedCounter, N>::const_iterator it = rhs.begin(); it != rhs.end(); ++it) { + if (!!*it) + o << *it << Endl; + } + return o; +} diff --git a/library/cpp/monlib/counters/counters_ut.cpp b/library/cpp/monlib/counters/counters_ut.cpp new file mode 100644 index 0000000000..2845efb97b --- /dev/null +++ b/library/cpp/monlib/counters/counters_ut.cpp @@ -0,0 +1,49 @@ +#include "counters.h" + +#include <library/cpp/testing/unittest/registar.h> + +#include <util/generic/set.h> +#include <util/thread/pool.h> + +using namespace NMonitoring; + +Y_UNIT_TEST_SUITE(TDeprecatedCountersTest) { + Y_UNIT_TEST(CounterGroupsAreThreadSafe) { + const static ui32 GROUPS_COUNT = 1000; + const static ui32 THREADS_COUNT = 10; + + TDeprecatedCounterGroups<ui32, ui32> groups; + + auto adder = [&groups]() { + for (ui32 id = 0; id < GROUPS_COUNT; id++) { + groups.Get(id); + + // adds contention + ::NanoSleep(42); + } + }; + + TThreadPool q; + q.Start(THREADS_COUNT); + for (ui32 i = 0; i < THREADS_COUNT; i++) { + q.SafeAddFunc(adder); + } + q.Stop(); + + // each group id is present + for (ui32 id = 0; id < GROUPS_COUNT; id++) { + UNIT_ASSERT(groups.Has(id)); + } + + // group names contains only appropriate ids + auto ids = groups.GetGroupsNames(); + for (ui32 id : *ids) { + UNIT_ASSERT(id < GROUPS_COUNT); + } + + // no duplication in group names + TSet<ui32> uniqueIds(ids->begin(), ids->end()); + UNIT_ASSERT_EQUAL(ids->size(), uniqueIds.size()); + UNIT_ASSERT_EQUAL(ids->size(), GROUPS_COUNT); + } +} diff --git a/library/cpp/monlib/counters/histogram.cpp b/library/cpp/monlib/counters/histogram.cpp new file mode 100644 index 0000000000..46cf4e6ec8 --- /dev/null +++ b/library/cpp/monlib/counters/histogram.cpp @@ -0,0 +1,40 @@ +#include "histogram.h" + +namespace NMonitoring { + void THistogramSnapshot::Print(IOutputStream* out) const { + (*out) << "mean: " << Mean + << ", stddev: " << StdDeviation + << ", min: " << Min + << ", max: " << Max + << ", 50%: " << Percentile50 + << ", 75%: " << Percentile75 + << ", 90%: " << Percentile90 + << ", 95%: " << Percentile95 + << ", 98%: " << Percentile98 + << ", 99%: " << Percentile99 + << ", 99.9%: " << Percentile999 + << ", count: " << TotalCount; + } + + void THdrHistogram::TakeSnaphot(THistogramSnapshot* snapshot) { + with_lock (Lock_) { + // TODO: get data with single traverse + snapshot->Mean = Data_.GetMean(); + snapshot->StdDeviation = Data_.GetStdDeviation(); + snapshot->Min = Data_.GetMin(); + snapshot->Max = Data_.GetMax(); + snapshot->Percentile50 = Data_.GetValueAtPercentile(50.0); + snapshot->Percentile75 = Data_.GetValueAtPercentile(75.0); + snapshot->Percentile90 = Data_.GetValueAtPercentile(90.0); + snapshot->Percentile95 = Data_.GetValueAtPercentile(95.0); + snapshot->Percentile98 = Data_.GetValueAtPercentile(98.0); + snapshot->Percentile99 = Data_.GetValueAtPercentile(99.0); + snapshot->Percentile999 = Data_.GetValueAtPercentile(99.9); + snapshot->TotalCount = Data_.GetTotalCount(); + + // cleanup histogram data + Data_.Reset(); + } + } + +} diff --git a/library/cpp/monlib/counters/histogram.h b/library/cpp/monlib/counters/histogram.h new file mode 100644 index 0000000000..96361b0023 --- /dev/null +++ b/library/cpp/monlib/counters/histogram.h @@ -0,0 +1,189 @@ +#pragma once + +#include <library/cpp/histogram/hdr/histogram.h> + +#include <util/system/spinlock.h> +#include <util/stream/output.h> + +namespace NMonitoring { + /** + * A statistical snapshot of values recorded in histogram. + */ + struct THistogramSnapshot { + double Mean; + double StdDeviation; + i64 Min; + i64 Max; + i64 Percentile50; + i64 Percentile75; + i64 Percentile90; + i64 Percentile95; + i64 Percentile98; + i64 Percentile99; + i64 Percentile999; + i64 TotalCount; + + void Print(IOutputStream* out) const; + }; + + /** + * Special counter which calculates the distribution of a value. + */ + class THdrHistogram { + public: + /** + * Construct a histogram given the Lowest and Highest values to be tracked + * and a number of significant decimal digits. Providing a + * lowestDiscernibleValue is useful in situations where the units used for + * the histogram's values are much smaller that the minimal accuracy + * required. E.g. when tracking time values stated in nanosecond units, + * where the minimal accuracy required is a microsecond, the proper value + * for lowestDiscernibleValue would be 1000. + * + * @param lowestDiscernibleValue The lowest value that can be discerned + * (distinguished from 0) by the histogram. Must be a positive + * integer that is >= 1. May be internally rounded down to nearest + * power of 2. + * + * @param highestTrackableValue The highest value to be tracked by the + * histogram. Must be a positive integer that is + * >= (2 * lowestDiscernibleValue). + * + * @param numberOfSignificantValueDigits Specifies the precision to use. + * This is the number of significant decimal digits to which the + * histogram will maintain value resolution and separation. Must be + * a non-negative integer between 0 and 5. + */ + THdrHistogram(i64 lowestDiscernibleValue, i64 highestTrackableValue, + i32 numberOfSignificantValueDigits) + : Data_(lowestDiscernibleValue, highestTrackableValue, + numberOfSignificantValueDigits) { + } + + /** + * Records a value in the histogram, will round this value of to a + * precision at or better than the NumberOfSignificantValueDigits specified + * at construction time. + * + * @param value Value to add to the histogram + * @return false if the value is larger than the HighestTrackableValue + * and can't be recorded, true otherwise. + */ + bool RecordValue(i64 value) { + with_lock (Lock_) { + return Data_.RecordValue(value); + } + } + + /** + * Records count values in the histogram, will round this value of to a + * precision at or better than the NumberOfSignificantValueDigits specified + * at construction time. + * + * @param value Value to add to the histogram + * @param count Number of values to add to the histogram + * @return false if the value is larger than the HighestTrackableValue + * and can't be recorded, true otherwise. + */ + bool RecordValues(i64 value, i64 count) { + with_lock (Lock_) { + return Data_.RecordValues(value, count); + } + } + + /** + * Records a value in the histogram and backfill based on an expected + * interval. Value will be rounded this to a precision at or better + * than the NumberOfSignificantValueDigits specified at contruction time. + * This is specifically used for recording latency. If the value is larger + * than the expectedInterval then the latency recording system has + * experienced co-ordinated omission. This method fills in the values that + * would have occured had the client providing the load not been blocked. + * + * @param value Value to add to the histogram + * @param expectedInterval The delay between recording values + * @return false if the value is larger than the HighestTrackableValue + * and can't be recorded, true otherwise. + */ + bool RecordValueWithExpectedInterval(i64 value, i64 expectedInterval) { + with_lock (Lock_) { + return Data_.RecordValueWithExpectedInterval(value, expectedInterval); + } + } + + /** + * Record a value in the histogram count times. Applies the same correcting + * logic as {@link THdrHistogram::RecordValueWithExpectedInterval}. + * + * @param value Value to add to the histogram + * @param count Number of values to add to the histogram + * @param expectedInterval The delay between recording values. + * @return false if the value is larger than the HighestTrackableValue + * and can't be recorded, true otherwise. + */ + bool RecordValuesWithExpectedInterval( + i64 value, i64 count, i64 expectedInterval) { + with_lock (Lock_) { + return Data_.RecordValuesWithExpectedInterval( + value, count, expectedInterval); + } + } + + /** + * @return The configured lowestDiscernibleValue + */ + i64 GetLowestDiscernibleValue() const { + with_lock (Lock_) { + return Data_.GetLowestDiscernibleValue(); + } + } + + /** + * @return The configured highestTrackableValue + */ + i64 GetHighestTrackableValue() const { + with_lock (Lock_) { + return Data_.GetHighestTrackableValue(); + } + } + + /** + * @return The configured numberOfSignificantValueDigits + */ + i32 GetNumberOfSignificantValueDigits() const { + with_lock (Lock_) { + return Data_.GetNumberOfSignificantValueDigits(); + } + } + + /** + * @return The total count of all recorded values in the histogram + */ + i64 GetTotalCount() const { + with_lock (Lock_) { + return Data_.GetTotalCount(); + } + } + + /** + * Place a copy of the value counts accumulated since the last snapshot + * was taken into {@code snapshot}. Calling this member-function will + * reset the value counts, and start accumulating value counts for the + * next interval. + * + * @param snapshot the structure into which the values should be copied. + */ + void TakeSnaphot(THistogramSnapshot* snapshot); + + private: + mutable TSpinLock Lock_; + NHdr::THistogram Data_; + }; + +} + +template <> +inline void Out<NMonitoring::THistogramSnapshot>( + IOutputStream& out, const NMonitoring::THistogramSnapshot& snapshot) { + snapshot.Print(&out); +} diff --git a/library/cpp/monlib/counters/histogram_ut.cpp b/library/cpp/monlib/counters/histogram_ut.cpp new file mode 100644 index 0000000000..5a0800505a --- /dev/null +++ b/library/cpp/monlib/counters/histogram_ut.cpp @@ -0,0 +1,47 @@ +#include "histogram.h" + +#include <library/cpp/testing/unittest/registar.h> + +using namespace NMonitoring; + +Y_UNIT_TEST_SUITE(THistorgamTest) { + Y_UNIT_TEST(TakeSnapshot) { + THdrHistogram h(1, 10, 3); + UNIT_ASSERT(h.RecordValue(1)); + UNIT_ASSERT(h.RecordValue(2)); + UNIT_ASSERT(h.RecordValues(3, 10)); + UNIT_ASSERT(h.RecordValue(4)); + UNIT_ASSERT(h.RecordValue(5)); + + UNIT_ASSERT_EQUAL(h.GetTotalCount(), 14); + + THistogramSnapshot snapshot; + h.TakeSnaphot(&snapshot); + + UNIT_ASSERT_EQUAL(h.GetTotalCount(), 0); + + UNIT_ASSERT_EQUAL(snapshot.Min, 1); + UNIT_ASSERT_EQUAL(snapshot.Max, 5); + + // >>> a = [1, 2] + [3 for i in range(10)] + [4, 5] + // >>> numpy.mean(a) + // 3.0 + UNIT_ASSERT_DOUBLES_EQUAL(snapshot.Mean, 3.0, 1e-6); + + // >>> numpy.std(a) + // 0.84515425472851657 + UNIT_ASSERT_DOUBLES_EQUAL(snapshot.StdDeviation, 0.84515425472851657, 1e-6); + + // >>> [(p, round(numpy.percentile(a, p))) for p in [50, 75, 90, 95, 98, 99, 99.9, 100]] + // [(50, 3.0), (75, 3.0), (90, 4.0), (95, 4.0), (98, 5.0), (99, 5.0), (99.9, 5.0), (100, 5.0)] + UNIT_ASSERT_EQUAL(snapshot.Percentile50, 3); + UNIT_ASSERT_EQUAL(snapshot.Percentile75, 3); + UNIT_ASSERT_EQUAL(snapshot.Percentile90, 4); + UNIT_ASSERT_EQUAL(snapshot.Percentile95, 4); + UNIT_ASSERT_EQUAL(snapshot.Percentile98, 5); + UNIT_ASSERT_EQUAL(snapshot.Percentile99, 5); + UNIT_ASSERT_EQUAL(snapshot.Percentile999, 5); + + UNIT_ASSERT_EQUAL(snapshot.TotalCount, 14); + } +} diff --git a/library/cpp/monlib/counters/meter.cpp b/library/cpp/monlib/counters/meter.cpp new file mode 100644 index 0000000000..6f15f173d1 --- /dev/null +++ b/library/cpp/monlib/counters/meter.cpp @@ -0,0 +1,4 @@ +#include "meter.h" + +namespace NMonitoring { +} diff --git a/library/cpp/monlib/counters/meter.h b/library/cpp/monlib/counters/meter.h new file mode 100644 index 0000000000..1219f95c4d --- /dev/null +++ b/library/cpp/monlib/counters/meter.h @@ -0,0 +1,285 @@ +#pragma once + +#include <util/system/types.h> +#include <util/generic/noncopyable.h> +#include <util/system/atomic.h> + +#include <chrono> +#include <cstdlib> +#include <cmath> + +namespace NMonitoring { + /** + * An exponentially-weighted moving average. + * + * @see <a href="http://www.teamquest.com/pdfs/whitepaper/ldavg1.pdf"> + * UNIX Load Average Part 1: How It Works</a> + * @see <a href="http://www.teamquest.com/pdfs/whitepaper/ldavg2.pdf"> + * UNIX Load Average Part 2: Not Your Average Average</a> + * @see <a href="http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average">EMA</a> + */ + class TMovingAverage { + public: + enum { + INTERVAL = 5 // in seconds + }; + + public: + /** + * Creates a new EWMA which is equivalent to the UNIX one minute load + * average and which expects to be ticked every 5 seconds. + * + * @return a one-minute EWMA + */ + static TMovingAverage OneMinute() { + static const double M1_ALPHA = 1 - std::exp(-INTERVAL / 60.0 / 1); + return {M1_ALPHA, std::chrono::seconds(INTERVAL)}; + } + + /** + * Creates a new EWMA which is equivalent to the UNIX five minute load + * average and which expects to be ticked every 5 seconds. + * + * @return a five-minute EWMA + */ + static TMovingAverage FiveMinutes() { + static const double M5_ALPHA = 1 - std::exp(-INTERVAL / 60.0 / 5); + return {M5_ALPHA, std::chrono::seconds(INTERVAL)}; + } + + /** + * Creates a new EWMA which is equivalent to the UNIX fifteen minute load + * average and which expects to be ticked every 5 seconds. + * + * @return a fifteen-minute EWMA + */ + static TMovingAverage FifteenMinutes() { + static const double M15_ALPHA = 1 - std::exp(-INTERVAL / 60.0 / 15); + return {M15_ALPHA, std::chrono::seconds(INTERVAL)}; + } + + /** + * Create a new EWMA with a specific smoothing constant. + * + * @param alpha the smoothing constant + * @param interval the expected tick interval + */ + TMovingAverage(double alpha, std::chrono::seconds interval) + : Initialized_(0) + , Rate_(0) + , Uncounted_(0) + , Alpha_(alpha) + , Interval_(std::chrono::nanoseconds(interval).count()) + { + } + + TMovingAverage(const TMovingAverage& rhs) + : Initialized_(AtomicGet(rhs.Initialized_)) + , Rate_(AtomicGet(rhs.Rate_)) + , Uncounted_(AtomicGet(rhs.Uncounted_)) + , Alpha_(rhs.Alpha_) + , Interval_(rhs.Interval_) + { + } + + TMovingAverage& operator=(const TMovingAverage& rhs) { + AtomicSet(Initialized_, AtomicGet(Initialized_)); + AtomicSet(Rate_, AtomicGet(rhs.Rate_)); + AtomicSet(Uncounted_, AtomicGet(rhs.Uncounted_)); + Alpha_ = rhs.Alpha_; + Interval_ = rhs.Interval_; + return *this; + } + + /** + * Update the moving average with a new value. + * + * @param n the new value + */ + void Update(ui64 n = 1) { + AtomicAdd(Uncounted_, n); + } + + /** + * Mark the passage of time and decay the current rate accordingly. + */ + void Tick() { + double instantRate = AtomicSwap(&Uncounted_, 0) / Interval_; + if (AtomicGet(Initialized_)) { + double rate = AsDouble(AtomicGet(Rate_)); + rate += (Alpha_ * (instantRate - rate)); + AtomicSet(Rate_, AsAtomic(rate)); + } else { + AtomicSet(Rate_, AsAtomic(instantRate)); + AtomicSet(Initialized_, 1); + } + } + + /** + * @return the rate in the seconds + */ + double GetRate() const { + double rate = AsDouble(AtomicGet(Rate_)); + return rate * std::nano::den; + } + + private: + static double AsDouble(TAtomicBase val) { + union { + double D; + TAtomicBase A; + } doubleAtomic; + doubleAtomic.A = val; + return doubleAtomic.D; + } + + static TAtomicBase AsAtomic(double val) { + union { + double D; + TAtomicBase A; + } doubleAtomic; + doubleAtomic.D = val; + return doubleAtomic.A; + } + + private: + TAtomic Initialized_; + TAtomic Rate_; + TAtomic Uncounted_; + double Alpha_; + double Interval_; + }; + + /** + * A meter metric which measures mean throughput and one-, five-, and + * fifteen-minute exponentially-weighted moving average throughputs. + */ + template <typename TClock> + class TMeterImpl: private TNonCopyable { + public: + TMeterImpl() + : StartTime_(TClock::now()) + , LastTick_(StartTime_.time_since_epoch().count()) + , Count_(0) + , OneMinuteRate_(TMovingAverage::OneMinute()) + , FiveMinutesRate_(TMovingAverage::FiveMinutes()) + , FifteenMinutesRate_(TMovingAverage::FifteenMinutes()) + { + } + + /** + * Mark the occurrence of events. + * + * @param n the number of events + */ + void Mark(ui64 n = 1) { + TickIfNecessary(); + AtomicAdd(Count_, n); + OneMinuteRate_.Update(n); + FiveMinutesRate_.Update(n); + FifteenMinutesRate_.Update(n); + } + + /** + * Returns the one-minute exponentially-weighted moving average rate at + * which events have occurred since the meter was created. + * + * This rate has the same exponential decay factor as the one-minute load + * average in the top Unix command. + * + * @return the one-minute exponentially-weighted moving average rate at + * which events have occurred since the meter was created + */ + double GetOneMinuteRate() const { + return OneMinuteRate_.GetRate(); + } + + /** + * Returns the five-minute exponentially-weighted moving average rate at + * which events have occurred since the meter was created. + * + * This rate has the same exponential decay factor as the five-minute load + * average in the top Unix command. + * + * @return the five-minute exponentially-weighted moving average rate at + * which events have occurred since the meter was created + */ + double GetFiveMinutesRate() const { + return FiveMinutesRate_.GetRate(); + } + + /** + * Returns the fifteen-minute exponentially-weighted moving average rate + * at which events have occurred since the meter was created. + * + * This rate has the same exponential decay factor as the fifteen-minute + * load average in the top Unix command. + * + * @return the fifteen-minute exponentially-weighted moving average rate + * at which events have occurred since the meter was created + */ + double GetFifteenMinutesRate() const { + return FifteenMinutesRate_.GetRate(); + } + + /** + * @return the mean rate at which events have occurred since the meter + * was created + */ + double GetMeanRate() const { + if (GetCount() == 0) { + return 0.0; + } + + auto now = TClock::now(); + std::chrono::duration<double> elapsedSeconds = now - StartTime_; + return GetCount() / elapsedSeconds.count(); + } + + /** + * @return the number of events which have been marked + */ + ui64 GetCount() const { + return AtomicGet(Count_); + } + + private: + void TickIfNecessary() { + static ui64 TICK_INTERVAL_NS = + std::chrono::nanoseconds( + std::chrono::seconds(TMovingAverage::INTERVAL)) + .count(); + + auto oldTickNs = AtomicGet(LastTick_); + auto newTickNs = TClock::now().time_since_epoch().count(); + ui64 elapsedNs = std::abs(newTickNs - oldTickNs); + + if (elapsedNs > TICK_INTERVAL_NS) { + // adjust to interval begining + newTickNs -= elapsedNs % TICK_INTERVAL_NS; + if (AtomicCas(&LastTick_, newTickNs, oldTickNs)) { + ui64 requiredTicks = elapsedNs / TICK_INTERVAL_NS; + for (ui64 i = 0; i < requiredTicks; ++i) { + OneMinuteRate_.Tick(); + FiveMinutesRate_.Tick(); + FifteenMinutesRate_.Tick(); + } + } + } + } + + private: + const typename TClock::time_point StartTime_; + TAtomic LastTick_; + TAtomic Count_; + TMovingAverage OneMinuteRate_; + TMovingAverage FiveMinutesRate_; + TMovingAverage FifteenMinutesRate_; + }; + + using TSystemMeter = TMeterImpl<std::chrono::system_clock>; + using TSteadyMeter = TMeterImpl<std::chrono::steady_clock>; + using THighResMeter = TMeterImpl<std::chrono::high_resolution_clock>; + using TMeter = THighResMeter; + +} diff --git a/library/cpp/monlib/counters/meter_ut.cpp b/library/cpp/monlib/counters/meter_ut.cpp new file mode 100644 index 0000000000..b507d16fbd --- /dev/null +++ b/library/cpp/monlib/counters/meter_ut.cpp @@ -0,0 +1,41 @@ +#include "meter.h" + +#include <library/cpp/testing/unittest/registar.h> + +using namespace NMonitoring; + +struct TMockClock { + using duration = std::chrono::nanoseconds; + using rep = duration::rep; + using period = duration::period; + using time_point = std::chrono::time_point<TMockClock, duration>; + + static time_point now() noexcept { + static int index = 0; + return index++ < 2 ? time_point() : time_point(std::chrono::seconds(10)); + } +}; + +using TMockMeter = TMeterImpl<TMockClock>; + +Y_UNIT_TEST_SUITE(TMeterTest) { + Y_UNIT_TEST(StartsOutWithNoRatesOrCount) { + TMeter meter; + UNIT_ASSERT_EQUAL(meter.GetCount(), 0L); + UNIT_ASSERT_DOUBLES_EQUAL(meter.GetMeanRate(), 0.0, 0.0001); + UNIT_ASSERT_DOUBLES_EQUAL(meter.GetOneMinuteRate(), 0.0, 0.0001); + UNIT_ASSERT_DOUBLES_EQUAL(meter.GetFiveMinutesRate(), 0.0, 0.0001); + UNIT_ASSERT_DOUBLES_EQUAL(meter.GetFifteenMinutesRate(), 0.0, 0.0001); + } + + Y_UNIT_TEST(MarksEventsAndUpdatesRatesAndCount) { + TMockMeter meter; + meter.Mark(); + meter.Mark(2); + UNIT_ASSERT_EQUAL(meter.GetCount(), 3L); + UNIT_ASSERT_DOUBLES_EQUAL(meter.GetMeanRate(), 0.3, 0.001); + UNIT_ASSERT_DOUBLES_EQUAL(meter.GetOneMinuteRate(), 0.1840, 0.0001); + UNIT_ASSERT_DOUBLES_EQUAL(meter.GetFiveMinutesRate(), 0.1966, 0.0001); + UNIT_ASSERT_DOUBLES_EQUAL(meter.GetFifteenMinutesRate(), 0.1988, 0.0001); + } +} diff --git a/library/cpp/monlib/counters/timer.h b/library/cpp/monlib/counters/timer.h new file mode 100644 index 0000000000..03dfb35337 --- /dev/null +++ b/library/cpp/monlib/counters/timer.h @@ -0,0 +1,176 @@ +#pragma once + +#include "histogram.h" + +#include <util/generic/scope.h> + +#include <chrono> + +namespace NMonitoring { + /** + * A timer counter which aggregates timing durations and provides duration + * statistics in selected time resolution. + */ + template <typename TResolution> + class TTimerImpl { + public: + /** + * Construct a timer given the Lowest and Highest values to be tracked + * and a number of significant decimal digits. Providing a + * lowestDiscernibleValue is useful in situations where the units used for + * the timer's values are much smaller that the minimal accuracy + * required. E.g. when tracking time values stated in nanosecond units, + * where the minimal accuracy required is a microsecond, the proper value + * for lowestDiscernibleValue would be 1000. + * + * @param min The lowest value that can be discerned (distinguished from + * 0) by the timer. Must be a positive integer that is >= 1. + * May be internally rounded down to nearest power of 2. + * + * @param max The highest value to be tracked by the timer. Must be a + * positive integer that is >= (2 * min). + * + * @param numberOfSignificantValueDigits Specifies the precision to use. + * This is the number of significant decimal digits to which the + * timer will maintain value resolution and separation. Must be + * a non-negative integer between 0 and 5. + */ + TTimerImpl(ui64 min, ui64 max, i32 numberOfSignificantValueDigits = 3) + : TTimerImpl(TResolution(min), TResolution(max), + numberOfSignificantValueDigits) { + } + + /** + * Construct a timer given the Lowest and Highest values to be tracked + * and a number of significant decimal digits. + * + * @param min The lowest value that can be discerned (distinguished from + * 0) by the timer. + * + * @param max The highest value to be tracked by the histogram. Must be a + * positive integer that is >= (2 * min). + * + * @param numberOfSignificantValueDigits Specifies the precision to use. + */ + template <typename TDurationMin, typename TDurationMax> + TTimerImpl(TDurationMin min, TDurationMax max, + i32 numberOfSignificantValueDigits = 3) + : Histogram_(std::chrono::duration_cast<TResolution>(min).count(), + std::chrono::duration_cast<TResolution>(max).count(), + numberOfSignificantValueDigits) { + } + + /** + * Records a value in the timer with timer resulution. Recorded value will + * be rounded to a precision at or better than the + * NumberOfSignificantValueDigits specified at construction time. + * + * @param duration duration to add to the timer + * @return false if the value is larger than the max and can't be recorded, + * true otherwise. + */ + bool RecordValue(ui64 duration) { + return Histogram_.RecordValue(duration); + } + + /** + * Records a duration in the timer. Recorded value will be converted to + * the timer resulution and rounded to a precision at or better than the + * NumberOfSignificantValueDigits specified at construction time. + * + * @param duration duration to add to the timer + * @return false if the value is larger than the max and can't be recorded, + * true otherwise. + */ + template <typename TDuration> + bool RecordValue(TDuration duration) { + auto count = static_cast<ui64>( + std::chrono::duration_cast<TResolution>(duration).count()); + return RecordValue(count); + } + + /** + * Records count values in the timer with timer resulution. Recorded value will + * be rounded to a precision at or better than the + * NumberOfSignificantValueDigits specified at construction time. + * + * @param duration duration to add to the timer + * @param count number of values to add to the histogram + * @return false if the value is larger than the max and can't be recorded, + * true otherwise. + */ + bool RecordValues(ui64 duration, ui64 count) { + return Histogram_.RecordValues(duration, count); + } + + /** + * Measures a time of functor execution. + * + * @param fn functor whose duration should be timed + */ + template <typename TFunc> + void Measure(TFunc&& fn) { + using TClock = std::chrono::high_resolution_clock; + + auto start = TClock::now(); + + Y_SCOPE_EXIT(this, start) { + RecordValue(TClock::now() - start); + }; + + fn(); + } + + /** + * Place a copy of the value counts accumulated since the last snapshot + * was taken into {@code snapshot}. Calling this member-function will + * reset the value counts, and start accumulating value counts for the + * next interval. + * + * @param snapshot the structure into which the values should be copied. + */ + void TakeSnapshot(THistogramSnapshot* snapshot) { + Histogram_.TakeSnaphot(snapshot); + } + + private: + THdrHistogram Histogram_; + }; + + /** + * Timer template instantiations for certain time resolutions. + */ + using TTimerNs = TTimerImpl<std::chrono::nanoseconds>; + using TTimerUs = TTimerImpl<std::chrono::microseconds>; + using TTimerMs = TTimerImpl<std::chrono::milliseconds>; + using TTimerS = TTimerImpl<std::chrono::seconds>; + + /** + * A timing scope to record elapsed time since creation. + */ + template <typename TTimer, typename TFunc = std::function<void(std::chrono::high_resolution_clock::duration)>> + class TTimerScope { + using TClock = std::chrono::high_resolution_clock; + + public: + explicit TTimerScope(TTimer* timer, TFunc* callback = nullptr) + : Timer_(timer) + , StartTime_(TClock::now()) + , Callback_(callback) + { + } + + ~TTimerScope() { + TClock::duration duration = TClock::now() - StartTime_; + if (Callback_) { + (*Callback_)(duration); + } + Timer_->RecordValue(duration); + } + + private: + TTimer* Timer_; + TClock::time_point StartTime_; + TFunc* Callback_; + }; +} diff --git a/library/cpp/monlib/counters/timer_ut.cpp b/library/cpp/monlib/counters/timer_ut.cpp new file mode 100644 index 0000000000..c5cd07e89d --- /dev/null +++ b/library/cpp/monlib/counters/timer_ut.cpp @@ -0,0 +1,81 @@ +#include "timer.h" + +#include <library/cpp/testing/unittest/registar.h> + +using namespace NMonitoring; +using namespace std::literals::chrono_literals; + +class TCallback { +public: + explicit TCallback(int value) + : Value_(value){}; + void operator()(std::chrono::high_resolution_clock::duration duration) { + Value_ = duration.count(); + }; + + int Value_; +}; + +Y_UNIT_TEST_SUITE(TTimerTest) { + Y_UNIT_TEST(RecordValue) { + TTimerNs timerNs(1ns, 1s); + UNIT_ASSERT(timerNs.RecordValue(10us)); + + TTimerUs timerUs(1us, 1s); + UNIT_ASSERT(timerUs.RecordValue(10us)); + + THistogramSnapshot snapshot; + timerNs.TakeSnapshot(&snapshot); + UNIT_ASSERT_EQUAL(snapshot.Min, 10000); + UNIT_ASSERT_EQUAL(snapshot.Max, 10007); + UNIT_ASSERT_DOUBLES_EQUAL(snapshot.StdDeviation, 0.0, 1e-6); + + timerUs.TakeSnapshot(&snapshot); + UNIT_ASSERT_EQUAL(snapshot.Min, 10); + UNIT_ASSERT_EQUAL(snapshot.Max, 10); + UNIT_ASSERT_DOUBLES_EQUAL(snapshot.StdDeviation, 0.0, 1e-6); + } + + Y_UNIT_TEST(Measure) { + TTimerNs timer(1ns, 1s); + timer.Measure([]() { + Sleep(TDuration::MilliSeconds(1)); + }); + THistogramSnapshot snapshot; + timer.TakeSnapshot(&snapshot); + + UNIT_ASSERT(snapshot.Min > std::chrono::nanoseconds(1ms).count()); + UNIT_ASSERT(snapshot.Max > std::chrono::nanoseconds(1ms).count()); + UNIT_ASSERT_DOUBLES_EQUAL(snapshot.StdDeviation, 0.0, 1e-6); + } + + Y_UNIT_TEST(TimerScope) { + TTimerUs timer(1us, 1000s); + { + TTimerScope<TTimerUs> scope(&timer); + Sleep(TDuration::MilliSeconds(10)); + } + THistogramSnapshot snapshot; + timer.TakeSnapshot(&snapshot); + + UNIT_ASSERT(snapshot.Min > std::chrono::microseconds(10ms).count()); + UNIT_ASSERT(snapshot.Max > std::chrono::microseconds(10ms).count()); + UNIT_ASSERT_DOUBLES_EQUAL(snapshot.StdDeviation, 0.0, 1e-6); + } + + Y_UNIT_TEST(TimerScopeWithCallback) { + TCallback callback(0); + TTimerUs timer(1us, 1000s); + { + TTimerScope<TTimerUs, TCallback> scope(&timer, &callback); + Sleep(TDuration::MilliSeconds(10)); + } + THistogramSnapshot snapshot; + timer.TakeSnapshot(&snapshot); + + UNIT_ASSERT(snapshot.Min > std::chrono::microseconds(10ms).count()); + UNIT_ASSERT(snapshot.Max > std::chrono::microseconds(10ms).count()); + UNIT_ASSERT_DOUBLES_EQUAL(snapshot.StdDeviation, 0.0, 1e-6); + UNIT_ASSERT(callback.Value_ > std::chrono::microseconds(10ms).count()); + } +} diff --git a/library/cpp/monlib/counters/ut/ya.make b/library/cpp/monlib/counters/ut/ya.make new file mode 100644 index 0000000000..999dadb199 --- /dev/null +++ b/library/cpp/monlib/counters/ut/ya.make @@ -0,0 +1,12 @@ +UNITTEST_FOR(library/cpp/monlib/counters) + +OWNER(jamel) + +SRCS( + counters_ut.cpp + histogram_ut.cpp + meter_ut.cpp + timer_ut.cpp +) + +END() diff --git a/library/cpp/monlib/counters/ya.make b/library/cpp/monlib/counters/ya.make new file mode 100644 index 0000000000..aa1a671bf8 --- /dev/null +++ b/library/cpp/monlib/counters/ya.make @@ -0,0 +1,15 @@ +LIBRARY() + +OWNER(jamel) + +SRCS( + counters.cpp + histogram.cpp + meter.cpp +) + +PEERDIR( + library/cpp/histogram/hdr +) + +END() |