#1 - quicr module
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
project(tw_metrics)
|
||||
|
||||
add_subdirectory(tests)
|
||||
|
||||
file(GLOB HEADERS
|
||||
include/metrics/*.hpp
|
||||
)
|
||||
|
||||
add_library(${PROJECT_NAME} INTERFACE)
|
||||
add_library(tw::metrics ALIAS ${PROJECT_NAME})
|
||||
target_sources(${PROJECT_NAME}
|
||||
INTERFACE FILE_SET HEADERS
|
||||
BASE_DIRS include
|
||||
FILES ${HEADERS})
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
INTERFACE
|
||||
${PROJECT_SOURCE_DIR}/include/
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace tw::metrics {
|
||||
|
||||
/**
|
||||
* Aggregate of every value that fell into one bucket.
|
||||
*
|
||||
* Answers sum, average, minimum and maximum without keeping the individual
|
||||
* values. A sample nothing was added to reports zero for all of them, so gaps
|
||||
* read as zero rather than as an unset extreme.
|
||||
*/
|
||||
struct MetricSample {
|
||||
uint32_t count = 0;
|
||||
double sum = 0.0;
|
||||
double min = 0.0;
|
||||
double max = 0.0;
|
||||
|
||||
void add(double value) {
|
||||
if(count == 0) {
|
||||
min = value;
|
||||
max = value;
|
||||
} else {
|
||||
min = std::min(min, value);
|
||||
max = std::max(max, value);
|
||||
}
|
||||
|
||||
sum += value;
|
||||
count++;
|
||||
}
|
||||
|
||||
/** Folds `other` in, as if its values had been added to this sample. */
|
||||
void merge(const MetricSample& other) {
|
||||
if(other.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(count == 0) {
|
||||
*this = other;
|
||||
return;
|
||||
}
|
||||
|
||||
min = std::min(min, other.min);
|
||||
max = std::max(max, other.max);
|
||||
|
||||
sum += other.sum;
|
||||
count += other.count;
|
||||
}
|
||||
|
||||
double avg() const {
|
||||
return count == 0 ? 0.0 : sum / count;
|
||||
}
|
||||
|
||||
bool is_empty() const {
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
*this = {};
|
||||
}
|
||||
};
|
||||
|
||||
/** Statistic to read out of a sample. */
|
||||
enum class MetricField {
|
||||
Avg,
|
||||
Min,
|
||||
Max,
|
||||
Sum,
|
||||
Count
|
||||
};
|
||||
|
||||
inline double value_of(const MetricSample& sample, MetricField field) {
|
||||
switch(field) {
|
||||
case MetricField::Avg: return sample.avg();
|
||||
case MetricField::Min: return sample.min;
|
||||
case MetricField::Max: return sample.max;
|
||||
case MetricField::Sum: return sample.sum;
|
||||
case MetricField::Count: return (double)sample.count;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
#pragma once
|
||||
|
||||
#include "metrics/MetricSample.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace tw::metrics {
|
||||
|
||||
/**
|
||||
* Ring of samples, one bucket per `Interval` of elapsed time.
|
||||
*
|
||||
* Values pushed during the same interval fold into one bucket, and intervals
|
||||
* that pass without a value become empty buckets, so the distance between two
|
||||
* buckets always matches the time between them. Once `capacity` buckets are
|
||||
* held the oldest one is dropped.
|
||||
*/
|
||||
template<typename Interval, typename Clock = std::chrono::steady_clock>
|
||||
class MetricSeries {
|
||||
public:
|
||||
using TimePoint = typename Clock::time_point;
|
||||
|
||||
private:
|
||||
std::vector<MetricSample> m_buckets;
|
||||
|
||||
/** Index of the newest bucket, in `Interval` units since the clock epoch. */
|
||||
int64_t m_newest = 0;
|
||||
|
||||
/** Buckets holding data, counted back from the newest. */
|
||||
size_t m_count = 0;
|
||||
|
||||
static int64_t bucket_of(TimePoint time) {
|
||||
return (int64_t)std::chrono::floor<Interval>(time).time_since_epoch().count();
|
||||
}
|
||||
|
||||
size_t slot_of(int64_t index) const {
|
||||
int64_t size = (int64_t)m_buckets.size();
|
||||
int64_t slot = index % size;
|
||||
|
||||
return (size_t)(slot < 0 ? slot + size : slot);
|
||||
}
|
||||
|
||||
MetricSample& bucket_at(int64_t index) {
|
||||
return m_buckets[slot_of(index)];
|
||||
}
|
||||
|
||||
const MetricSample& bucket_at(int64_t index) const {
|
||||
return m_buckets[slot_of(index)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the newest bucket up to `index`, emptying every bucket the gap
|
||||
* covers. A gap wider than the ring empties all of it.
|
||||
*/
|
||||
void advance_to(int64_t index) {
|
||||
int64_t steps = index - m_newest;
|
||||
int64_t capacity = (int64_t)m_buckets.size();
|
||||
int64_t to_clear = std::min(steps, capacity);
|
||||
|
||||
for(int64_t i = 0; i < to_clear; i++) {
|
||||
bucket_at(index - i).reset();
|
||||
}
|
||||
|
||||
m_newest = index;
|
||||
m_count = steps >= capacity
|
||||
? m_buckets.size()
|
||||
: std::min(m_count + (size_t)steps, m_buckets.size());
|
||||
}
|
||||
|
||||
public:
|
||||
explicit MetricSeries(size_t capacity) :
|
||||
m_buckets(capacity)
|
||||
{
|
||||
if(capacity == 0) {
|
||||
throw std::invalid_argument("`capacity` must hold at least one bucket");
|
||||
}
|
||||
}
|
||||
|
||||
size_t capacity() const {
|
||||
return m_buckets.size();
|
||||
}
|
||||
|
||||
size_t size() const {
|
||||
return m_count;
|
||||
}
|
||||
|
||||
bool is_empty() const {
|
||||
return m_count == 0;
|
||||
}
|
||||
|
||||
/** Index of the newest bucket, in `Interval` units since the clock epoch. */
|
||||
int64_t newest_index() const {
|
||||
return m_newest;
|
||||
}
|
||||
|
||||
void push(double value) {
|
||||
push(value, Clock::now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `value` to the bucket `at` falls into. A value older than every
|
||||
* bucket still held is dropped.
|
||||
*/
|
||||
void push(double value, TimePoint at) {
|
||||
int64_t index = bucket_of(at);
|
||||
|
||||
if(m_count == 0) {
|
||||
m_newest = index;
|
||||
m_count = 1;
|
||||
|
||||
bucket_at(index).reset();
|
||||
bucket_at(index).add(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if(index > m_newest) {
|
||||
advance_to(index);
|
||||
} else if(m_newest - index >= (int64_t)m_count) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucket_at(index).add(value);
|
||||
}
|
||||
|
||||
/** Newest first: age 0 is the bucket currently being filled. */
|
||||
const MetricSample& at_age(size_t age) const {
|
||||
return bucket_at(m_newest - (int64_t)age);
|
||||
}
|
||||
|
||||
/** Aggregate of the newest `buckets` buckets. */
|
||||
MetricSample window(size_t buckets) const {
|
||||
MetricSample result;
|
||||
|
||||
size_t count = std::min(buckets, m_count);
|
||||
for(size_t age = 0; age < count; age++) {
|
||||
result.merge(at_age(age));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Aggregate of everything still held. */
|
||||
MetricSample window() const {
|
||||
return window(m_count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the newest `buckets` buckets into `xs` and `ys` oldest first, as
|
||||
* two contiguous arrays. `xs` holds the age of each bucket in `Interval`
|
||||
* units, so the bucket being filled sits at 0 and older ones run negative.
|
||||
* Both vectors are resized to the number of points written.
|
||||
*
|
||||
* `skip_newest` leaves that many of the newest buckets out. The bucket
|
||||
* being filled only holds the part of its interval that has elapsed, so
|
||||
* reading it next to whole ones makes the newest point dip and recover;
|
||||
* skipping it keeps every point covering the same span of time. Ages stay
|
||||
* true, so a skipped bucket leaves a gap rather than shifting the rest.
|
||||
*/
|
||||
size_t linearize(std::vector<double>& xs,
|
||||
std::vector<double>& ys,
|
||||
MetricField field,
|
||||
size_t buckets,
|
||||
size_t skip_newest = 0) const {
|
||||
size_t available = m_count > skip_newest ? m_count - skip_newest : 0;
|
||||
size_t count = std::min(buckets, available);
|
||||
|
||||
xs.resize(count);
|
||||
ys.resize(count);
|
||||
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
size_t age = skip_newest + count - 1 - i;
|
||||
|
||||
xs[i] = -(double)age;
|
||||
ys[i] = value_of(at_age(age), field);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
size_t linearize(std::vector<double>& xs,
|
||||
std::vector<double>& ys,
|
||||
MetricField field) const {
|
||||
return linearize(xs, ys, field, m_count);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
m_count = 0;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
project(tw_metrics_tests)
|
||||
|
||||
set(LIBS
|
||||
tw::metrics
|
||||
)
|
||||
|
||||
file(GLOB FILES
|
||||
./*.cpp
|
||||
)
|
||||
|
||||
add_executable(${PROJECT_NAME} ${FILES})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${LIBS}
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
|
||||
|
||||
include(CTest)
|
||||
include(Catch)
|
||||
|
||||
catch_discover_tests(${PROJECT_NAME})
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "metrics/MetricSample.hpp"
|
||||
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
|
||||
using tw::metrics::MetricField;
|
||||
using tw::metrics::MetricSample;
|
||||
using tw::metrics::value_of;
|
||||
|
||||
TEST_CASE("Empty sample reports zero", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
|
||||
REQUIRE(sample.is_empty());
|
||||
REQUIRE(sample.count == 0);
|
||||
REQUIRE(sample.sum == 0.0);
|
||||
REQUIRE(sample.min == 0.0);
|
||||
REQUIRE(sample.max == 0.0);
|
||||
REQUIRE(sample.avg() == 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Sample tracks sum, average and extremes", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
|
||||
sample.add(4.0);
|
||||
sample.add(1.0);
|
||||
sample.add(7.0);
|
||||
|
||||
REQUIRE(sample.count == 3);
|
||||
REQUIRE(sample.sum == 12.0);
|
||||
REQUIRE(sample.min == 1.0);
|
||||
REQUIRE(sample.max == 7.0);
|
||||
REQUIRE(sample.avg() == 4.0);
|
||||
}
|
||||
|
||||
TEST_CASE("First value sets both extremes", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
|
||||
sample.add(-5.0);
|
||||
|
||||
REQUIRE(sample.min == -5.0);
|
||||
REQUIRE(sample.max == -5.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Merge folds one sample into another", "[metric_sample]") {
|
||||
MetricSample left;
|
||||
left.add(2.0);
|
||||
left.add(4.0);
|
||||
|
||||
MetricSample right;
|
||||
right.add(10.0);
|
||||
right.add(0.5);
|
||||
|
||||
left.merge(right);
|
||||
|
||||
REQUIRE(left.count == 4);
|
||||
REQUIRE(left.sum == 16.5);
|
||||
REQUIRE(left.min == 0.5);
|
||||
REQUIRE(left.max == 10.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Merging with an empty sample changes nothing", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
sample.add(3.0);
|
||||
|
||||
sample.merge(MetricSample{});
|
||||
|
||||
REQUIRE(sample.count == 1);
|
||||
REQUIRE(sample.min == 3.0);
|
||||
REQUIRE(sample.max == 3.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Merging into an empty sample adopts the other", "[metric_sample]") {
|
||||
MetricSample other;
|
||||
other.add(3.0);
|
||||
other.add(9.0);
|
||||
|
||||
MetricSample sample;
|
||||
sample.merge(other);
|
||||
|
||||
REQUIRE(sample.count == 2);
|
||||
REQUIRE(sample.sum == 12.0);
|
||||
REQUIRE(sample.min == 3.0);
|
||||
REQUIRE(sample.max == 9.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Field selects the statistic to read", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
sample.add(2.0);
|
||||
sample.add(6.0);
|
||||
|
||||
REQUIRE(value_of(sample, MetricField::Avg) == 4.0);
|
||||
REQUIRE(value_of(sample, MetricField::Min) == 2.0);
|
||||
REQUIRE(value_of(sample, MetricField::Max) == 6.0);
|
||||
REQUIRE(value_of(sample, MetricField::Sum) == 8.0);
|
||||
REQUIRE(value_of(sample, MetricField::Count) == 2.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Reset empties the sample", "[metric_sample]") {
|
||||
MetricSample sample;
|
||||
sample.add(5.0);
|
||||
|
||||
sample.reset();
|
||||
|
||||
REQUIRE(sample.is_empty());
|
||||
REQUIRE(sample.max == 0.0);
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
#include "metrics/MetricSeries.hpp"
|
||||
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using tw::metrics::MetricField;
|
||||
using tw::metrics::MetricSeries;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Series = MetricSeries<std::chrono::seconds>;
|
||||
|
||||
/** Fixed origin so every test drives the series by hand. */
|
||||
static Clock::time_point at(int64_t seconds) {
|
||||
return Clock::time_point{} + std::chrono::hours(1) + std::chrono::seconds(seconds);
|
||||
}
|
||||
|
||||
TEST_CASE("Series starts empty", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
REQUIRE(series.capacity() == 8);
|
||||
REQUIRE(series.size() == 0);
|
||||
REQUIRE(series.is_empty());
|
||||
REQUIRE(series.window().is_empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Series rejects a zero capacity", "[metric_series]") {
|
||||
REQUIRE_THROWS_AS(Series(0), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE("Values in the same interval fold into one bucket", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(3.0, at(0));
|
||||
|
||||
REQUIRE(series.size() == 1);
|
||||
REQUIRE(series.at_age(0).count == 2);
|
||||
REQUIRE(series.at_age(0).avg() == 2.0);
|
||||
REQUIRE(series.at_age(0).min == 1.0);
|
||||
REQUIRE(series.at_age(0).max == 3.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Values in different intervals land in different buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(5.0, at(1));
|
||||
|
||||
REQUIRE(series.size() == 2);
|
||||
REQUIRE(series.at_age(0).sum == 5.0);
|
||||
REQUIRE(series.at_age(1).sum == 1.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Intervals without a value become empty buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(4.0, at(3));
|
||||
|
||||
REQUIRE(series.size() == 4);
|
||||
REQUIRE(series.at_age(0).sum == 4.0);
|
||||
REQUIRE(series.at_age(1).is_empty());
|
||||
REQUIRE(series.at_age(2).is_empty());
|
||||
REQUIRE(series.at_age(3).sum == 1.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Series never holds more than its capacity", "[metric_series]") {
|
||||
Series series(4);
|
||||
|
||||
for(int64_t i = 0; i < 10; i++) {
|
||||
series.push((double)i, at(i));
|
||||
}
|
||||
|
||||
REQUIRE(series.size() == 4);
|
||||
REQUIRE(series.at_age(0).sum == 9.0);
|
||||
REQUIRE(series.at_age(3).sum == 6.0);
|
||||
}
|
||||
|
||||
TEST_CASE("A gap wider than the ring leaves only the newest bucket filled", "[metric_series]") {
|
||||
Series series(4);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(100));
|
||||
|
||||
REQUIRE(series.size() == 4);
|
||||
REQUIRE(series.at_age(0).sum == 2.0);
|
||||
REQUIRE(series.at_age(1).is_empty());
|
||||
REQUIRE(series.at_age(2).is_empty());
|
||||
REQUIRE(series.at_age(3).is_empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Buckets dropped by wrapping do not come back", "[metric_series]") {
|
||||
Series series(4);
|
||||
|
||||
series.push(100.0, at(0));
|
||||
|
||||
for(int64_t i = 1; i < 5; i++) {
|
||||
series.push(1.0, at(i));
|
||||
}
|
||||
|
||||
REQUIRE(series.window().max == 1.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Window aggregates across buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(4.0, at(0));
|
||||
series.push(1.0, at(1));
|
||||
series.push(7.0, at(2));
|
||||
|
||||
auto window = series.window();
|
||||
|
||||
REQUIRE(window.count == 3);
|
||||
REQUIRE(window.sum == 12.0);
|
||||
REQUIRE(window.min == 1.0);
|
||||
REQUIRE(window.max == 7.0);
|
||||
REQUIRE(window.avg() == 4.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Window can be narrowed to the newest buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(4.0, at(0));
|
||||
series.push(1.0, at(1));
|
||||
series.push(7.0, at(2));
|
||||
|
||||
auto window = series.window(2);
|
||||
|
||||
REQUIRE(window.count == 2);
|
||||
REQUIRE(window.min == 1.0);
|
||||
REQUIRE(window.max == 7.0);
|
||||
}
|
||||
|
||||
TEST_CASE("Empty buckets do not skew the window extremes", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(5.0, at(0));
|
||||
series.push(9.0, at(4));
|
||||
|
||||
auto window = series.window();
|
||||
|
||||
REQUIRE(window.count == 2);
|
||||
REQUIRE(window.min == 5.0);
|
||||
REQUIRE(window.max == 9.0);
|
||||
}
|
||||
|
||||
TEST_CASE("A late value folds into the bucket it belongs to", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(2));
|
||||
series.push(6.0, at(1));
|
||||
|
||||
REQUIRE(series.size() == 3);
|
||||
REQUIRE(series.at_age(1).sum == 6.0);
|
||||
REQUIRE(series.at_age(0).sum == 2.0);
|
||||
}
|
||||
|
||||
TEST_CASE("A value older than every bucket held is dropped", "[metric_series]") {
|
||||
Series series(4);
|
||||
|
||||
for(int64_t i = 0; i < 4; i++) {
|
||||
series.push(1.0, at(i));
|
||||
}
|
||||
|
||||
series.push(99.0, at(-10));
|
||||
|
||||
REQUIRE(series.size() == 4);
|
||||
REQUIRE(series.window().max == 1.0);
|
||||
REQUIRE(series.window().count == 4);
|
||||
}
|
||||
|
||||
TEST_CASE("Linearize writes buckets oldest first", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(1));
|
||||
series.push(3.0, at(2));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
size_t count = series.linearize(xs, ys, MetricField::Sum);
|
||||
|
||||
REQUIRE(count == 3);
|
||||
REQUIRE(xs == std::vector<double>{-2.0, -1.0, 0.0});
|
||||
REQUIRE(ys == std::vector<double>{1.0, 2.0, 3.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Linearize can be limited to the newest buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(1));
|
||||
series.push(3.0, at(2));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
size_t count = series.linearize(xs, ys, MetricField::Sum, 2);
|
||||
|
||||
REQUIRE(count == 2);
|
||||
REQUIRE(xs == std::vector<double>{-1.0, 0.0});
|
||||
REQUIRE(ys == std::vector<double>{2.0, 3.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Linearize can leave out the newest buckets", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(1));
|
||||
series.push(3.0, at(2));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
size_t count = series.linearize(xs, ys, MetricField::Sum, 8, 1);
|
||||
|
||||
REQUIRE(count == 2);
|
||||
REQUIRE(ys == std::vector<double>{1.0, 2.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Skipping the newest bucket keeps the ages of the rest", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(1));
|
||||
series.push(3.0, at(2));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
series.linearize(xs, ys, MetricField::Sum, 8, 1);
|
||||
|
||||
REQUIRE(xs == std::vector<double>{-2.0, -1.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Skipping more buckets than are held writes nothing", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
size_t count = series.linearize(xs, ys, MetricField::Sum, 8, 4);
|
||||
|
||||
REQUIRE(count == 0);
|
||||
REQUIRE(xs.empty());
|
||||
REQUIRE(ys.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("A limit counts buckets that were not skipped", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
for(int64_t i = 0; i < 5; i++) {
|
||||
series.push((double)i, at(i));
|
||||
}
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
size_t count = series.linearize(xs, ys, MetricField::Sum, 2, 1);
|
||||
|
||||
REQUIRE(count == 2);
|
||||
REQUIRE(ys == std::vector<double>{2.0, 3.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Linearize reports empty buckets as zero", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(5.0, at(0));
|
||||
series.push(9.0, at(2));
|
||||
|
||||
std::vector<double> xs;
|
||||
std::vector<double> ys;
|
||||
|
||||
series.linearize(xs, ys, MetricField::Max);
|
||||
|
||||
REQUIRE(ys == std::vector<double>{5.0, 0.0, 9.0});
|
||||
}
|
||||
|
||||
TEST_CASE("Linearize resizes the vectors it is given", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
|
||||
std::vector<double> xs(64, 7.0);
|
||||
std::vector<double> ys(64, 7.0);
|
||||
|
||||
series.linearize(xs, ys, MetricField::Avg);
|
||||
|
||||
REQUIRE(xs.size() == 1);
|
||||
REQUIRE(ys.size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Clear empties the series but keeps its capacity", "[metric_series]") {
|
||||
Series series(8);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.clear();
|
||||
|
||||
REQUIRE(series.is_empty());
|
||||
REQUIRE(series.capacity() == 8);
|
||||
|
||||
series.push(2.0, at(1));
|
||||
|
||||
REQUIRE(series.size() == 1);
|
||||
REQUIRE(series.at_age(0).sum == 2.0);
|
||||
}
|
||||
|
||||
TEST_CASE("A coarser interval folds more values together", "[metric_series]") {
|
||||
MetricSeries<std::chrono::minutes> series(4);
|
||||
|
||||
series.push(1.0, at(0));
|
||||
series.push(2.0, at(30));
|
||||
series.push(3.0, at(90));
|
||||
|
||||
REQUIRE(series.size() == 2);
|
||||
REQUIRE(series.at_age(1).count == 2);
|
||||
REQUIRE(series.at_age(0).sum == 3.0);
|
||||
}
|
||||
Reference in New Issue
Block a user