diff --git a/Makefile.am b/Makefile.am index fdc2908f53..a6706f7848 100644 --- a/Makefile.am +++ b/Makefile.am @@ -123,7 +123,7 @@ sbin_PROGRAMS = \ bin_PROGRAMS = \ collectd-nagios \ collectd-tg \ - collectdctl + collectdctl endif # BUILD_WIN32 @@ -151,6 +151,7 @@ check_LTLIBRARIES = \ check_PROGRAMS = \ test_common \ + test_distribution \ test_format_graphite \ test_meta_data \ test_metric \ @@ -356,6 +357,11 @@ test_meta_data_SOURCES = \ src/testing.h test_meta_data_LDADD = libmetadata.la libplugin_mock.la +test_distribution_SOURCES = \ + src/daemon/distribution_test.c \ + src/testing.h +test_distribution_LDADD = libmetric.la libplugin_mock.la + test_metric_SOURCES = \ src/daemon/metric_test.c \ src/testing.h @@ -426,9 +432,11 @@ libmetadata_la_SOURCES = \ src/utils/metadata/meta_data.h libmetric_la_SOURCES = \ + src/daemon/distribution.c \ + src/daemon/distribution.h \ src/daemon/metric.c \ src/daemon/metric.h -libmetric_la_LIBADD = libmetadata.la $(COMMON_LIBS) +libmetric_la_LIBADD = libmetadata.la $(COMMON_LIBS) -lm libplugin_mock_la_SOURCES = \ src/daemon/plugin_mock.c \ diff --git a/src/daemon/distribution.c b/src/daemon/distribution.c new file mode 100644 index 0000000000..2297914461 --- /dev/null +++ b/src/daemon/distribution.c @@ -0,0 +1,306 @@ +/** + * collectd - src/daemon/distribution.c + * Copyright (C) 2019-2020 Google LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * Authors: + * Svetlana Shmidt + **/ + +#include "distribution.h" + +#include + +struct distribution_s { + bucket_t *tree; + size_t num_buckets; + double total_sum; + pthread_mutex_t mutex; +}; + +/** + * This code uses an Euler path to avoid gaps in the tree-to-array mapping. + * This way the tree contained N buckets contains 2 * N - 1 nodes + * Thus, left subtree has 2 * (mid - left + 1) - 1 nodes, + * therefore the right subtree starts at node_index + 2 * (mid - left + 1). + * For a detailed explanation, see + * https://docs.google.com/document/d/1ccsg5ffUfqt9-mBDGTymRn8X-9Wk1CuGYeMlRxmxiok/edit?usp=sharing". + */ + +static size_t left_child_index(size_t node_index, + __attribute__((unused)) size_t left, + __attribute__((unused)) size_t right) { + return node_index + 1; +} + +static size_t right_child_index(size_t node_index, size_t left, size_t right) { + size_t mid = (left + right) / 2; + return node_index + 2 * (mid - left + 1); +} + +static size_t tree_size(size_t num_buckets) { return 2 * num_buckets - 1; } + +static bucket_t merge_buckets(bucket_t left_child, bucket_t right_child) { + return (bucket_t){ + .bucket_counter = left_child.bucket_counter + right_child.bucket_counter, + .maximum = right_child.maximum, + }; +} + +static void build_tree(distribution_t *d, bucket_t *buckets, size_t node_index, + size_t left, size_t right) { + if (left > right) + return; + if (left == right) { + d->tree[node_index] = buckets[left]; + return; + } + size_t mid = (left + right) / 2; + size_t left_child = left_child_index(node_index, left, right); + size_t right_child = right_child_index(node_index, left, right); + build_tree(d, buckets, left_child, left, mid); + build_tree(d, buckets, right_child, mid + 1, right); + d->tree[node_index] = + merge_buckets(d->tree[left_child], d->tree[right_child]); +} + +static distribution_t * +build_distribution_from_bucket_array(size_t num_buckets, + bucket_t *bucket_array) { + distribution_t *new_distribution = calloc(1, sizeof(*new_distribution)); + bucket_t *nodes = calloc(tree_size(num_buckets), sizeof(*nodes)); + if (new_distribution == NULL || nodes == NULL) { + free(new_distribution); + free(nodes); + return NULL; + } + new_distribution->tree = nodes; + + new_distribution->num_buckets = num_buckets; + build_tree(new_distribution, bucket_array, 0, 0, num_buckets - 1); + pthread_mutex_init(&new_distribution->mutex, NULL); + return new_distribution; +} + +distribution_t *distribution_new_linear(size_t num_buckets, double size) { + if (num_buckets == 0 || size <= 0) { + errno = EINVAL; + return NULL; + } + + bucket_t bucket_array[num_buckets]; + for (size_t i = 0; i < num_buckets; i++) { + bucket_array[i] = (bucket_t){ + .bucket_counter = 0, + .maximum = (i == num_buckets - 1) ? INFINITY : (i + 1) * size, + }; + } + return build_distribution_from_bucket_array(num_buckets, bucket_array); +} + +distribution_t *distribution_new_exponential(size_t num_buckets, double base, + double factor) { + if (num_buckets == 0 || base <= 1 || factor <= 0) { + errno = EINVAL; + return NULL; + } + + bucket_t bucket_array[num_buckets]; + for (size_t i = 0; i < num_buckets; i++) { + bucket_array[i] = (bucket_t){ + .bucket_counter = 0, + .maximum = (i == num_buckets - 1) + ? INFINITY + : factor * pow(base, i), // check if it's slow + }; + } + return build_distribution_from_bucket_array(num_buckets, bucket_array); +} + +distribution_t *distribution_new_custom(size_t array_size, + double *custom_buckets_boundaries) { + for (size_t i = 0; i < array_size; i++) { + double previous_boundary = 0; + if (i > 0) { + previous_boundary = custom_buckets_boundaries[i - 1]; + } + if (custom_buckets_boundaries[i] <= previous_boundary) { + errno = EINVAL; + return NULL; + } + } + if (array_size > 0 && custom_buckets_boundaries[array_size - 1] == INFINITY) { + errno = EINVAL; + return NULL; + } + + size_t num_buckets = array_size + 1; + bucket_t bucket_array[num_buckets]; + for (size_t i = 0; i < num_buckets; i++) { + bucket_array[i] = (bucket_t){ + .bucket_counter = 0, + .maximum = + (i == num_buckets - 1) ? INFINITY : custom_buckets_boundaries[i], + }; + } + return build_distribution_from_bucket_array(num_buckets, bucket_array); +} + +void distribution_destroy(distribution_t *d) { + if (d == NULL) + return; + pthread_mutex_destroy(&d->mutex); + free(d->tree); + free(d); +} + +distribution_t *distribution_clone(distribution_t *dist) { + if (dist == NULL) + return NULL; + distribution_t *new_distribution = calloc(1, sizeof(*new_distribution)); + bucket_t *nodes = calloc(tree_size(dist->num_buckets), sizeof(*nodes)); + if (new_distribution == NULL || nodes == NULL) { + free(new_distribution); + free(nodes); + return NULL; + } + pthread_mutex_lock(&dist->mutex); + memcpy(nodes, dist->tree, tree_size(dist->num_buckets) * sizeof(bucket_t)); + new_distribution->num_buckets = dist->num_buckets; + new_distribution->total_sum = dist->total_sum; + pthread_mutex_unlock(&dist->mutex); + new_distribution->tree = nodes; + pthread_mutex_init(&new_distribution->mutex, NULL); + return new_distribution; +} + +static void update_tree(distribution_t *dist, size_t node_index, size_t left, + size_t right, double gauge) { + if (left > right) + return; + dist->tree[node_index].bucket_counter++; + if (left == right) { + return; + } + size_t mid = (left + right) / 2; + size_t left_child = left_child_index(node_index, left, right); + size_t right_child = right_child_index(node_index, left, right); + if (dist->tree[left_child].maximum > gauge) + update_tree(dist, left_child, left, mid, gauge); + else + update_tree(dist, right_child, mid + 1, right, gauge); +} + +void distribution_update(distribution_t *dist, double gauge) { + if (dist == NULL) + return; + if (gauge < 0) { + errno = EINVAL; + return; + } + pthread_mutex_lock(&dist->mutex); + update_tree(dist, 0, 0, dist->num_buckets - 1, gauge); + dist->total_sum += gauge; + pthread_mutex_unlock(&dist->mutex); +} + +static double tree_get_counter(distribution_t *d, size_t node_index, + size_t left, size_t right, uint64_t counter) { + if (left > right) + return NAN; + if (left == right) { + return d->tree[node_index].maximum; + } + size_t mid = (left + right) / 2; + size_t left_child = left_child_index(node_index, left, right); + size_t right_child = right_child_index(node_index, left, right); + if (d->tree[left_child].bucket_counter >= counter) + return tree_get_counter(d, left_child, left, mid, counter); + else + return tree_get_counter(d, right_child, mid + 1, right, + counter - d->tree[left_child].bucket_counter); +} + +double distribution_percentile(distribution_t *dist, double percent) { + if (percent <= 0 || percent > 100) { + errno = EINVAL; + return NAN; + } + pthread_mutex_lock(&dist->mutex); + if (dist->tree[0].bucket_counter == 0) + return NAN; + uint64_t counter = ceil(dist->tree[0].bucket_counter * percent / 100.0); + double percentile = + tree_get_counter(dist, 0, 0, dist->num_buckets - 1, counter); + pthread_mutex_unlock(&dist->mutex); + return percentile; +} + +double distribution_average(distribution_t *dist) { + pthread_mutex_lock(&dist->mutex); + if (dist == NULL || dist->tree[0].bucket_counter == 0) { + return NAN; + } + double average = dist->total_sum / dist->tree[0].bucket_counter; + pthread_mutex_unlock(&dist->mutex); + return average; +} + +size_t distribution_num_buckets(distribution_t *dist) { + if (dist == NULL) + return 0; + return dist->num_buckets; +} + +static void tree_write_leave_buckets(distribution_t *dist, bucket_t *write_ptr, + size_t node_index, size_t left, + size_t right) { + if (left > right) + return; + if (left == right) { + write_ptr[left] = dist->tree[node_index]; + return; + } + size_t mid = (left + right) / 2; + size_t left_child = left_child_index(node_index, left, right); + size_t right_child = right_child_index(node_index, left, right); + tree_write_leave_buckets(dist, write_ptr, left_child, left, mid); + tree_write_leave_buckets(dist, write_ptr, right_child, mid + 1, right); +} + +buckets_array_t get_buckets(distribution_t *dist) { + buckets_array_t bucket_array = { + .num_buckets = dist == NULL ? 0 : dist->num_buckets, + .buckets = dist == NULL + ? NULL + : calloc(dist->num_buckets, sizeof(*bucket_array.buckets)), + }; + if (dist == NULL) + return bucket_array; + bucket_t *write_ptr = bucket_array.buckets; + pthread_mutex_lock(&dist->mutex); + tree_write_leave_buckets(dist, write_ptr, 0, 0, dist->num_buckets - 1); + pthread_mutex_unlock(&dist->mutex); + return bucket_array; +} + +void destroy_buckets_array(buckets_array_t buckets_array) { + free(buckets_array.buckets); +} diff --git a/src/daemon/distribution.h b/src/daemon/distribution.h new file mode 100644 index 0000000000..fdb771293c --- /dev/null +++ b/src/daemon/distribution.h @@ -0,0 +1,115 @@ +/** + * collectd - src/daemon/distribution.h + * Copyright (C) 2019-2020 Google LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * Authors: + * Svetlana Shmidt + **/ + +#ifndef COLLECTD_DISTRIBUTION_H +#define COLLECTD_DISTRIBUTION_H + +#include "collectd.h" + +typedef struct bucket_s { + uint64_t bucket_counter; + double maximum; +} bucket_t; + +struct distribution_s; +typedef struct distribution_s distribution_t; + +typedef struct buckets_array_s { + size_t num_buckets; + bucket_t *buckets; +} buckets_array_t; + +// constructor functions: +/** + * function creates new distribution with the linear buckets: + * [0; size) [size; 2 * size) ... [(num_buckets - 1) * size; infinity) + * @param num_buckets - number of buckets. Should be greater than 0 + * @param size - size of each bucket. Should be greater than 0 + * @return - pointer to a new distribution or null pointer if parameters are + * wrong or memory allocation fails + */ +distribution_t *distribution_new_linear(size_t num_buckets, double size); + +/** + * function creates new distribution with the exponential buckets: + * [0; factor) [factor; factor * base) ... [factor * base^{num_buckets - 2}; + * infinity) + * @param num_buckets - number of buckets. Should be greater than 0 + * @param base - base of geometric progression. Should be greater than 1 + * @param factor - size of the first bucket. Should be greater than 0 + * @return - pointer to a new distribution or null pointer if parameters are + * wrong or memory allocation fails + */ +distribution_t *distribution_new_exponential(size_t num_buckets, double base, + double factor); + +/** + * function creates new distribution with the custom buckets: + * [0; custom_bucket_boundaries[0]) [custom_bucket_boundaries[0]; + * custom_bucket_boundaries[1]) ... + * ... [custom_bucket_boundaries[array_size - 1], infinity) + * @param array_size - size of array of bucket boundaries. Number of buckets is + * array_size + 1 + * @param custom_buckets_boundaries - array with bucket boundaries. Should be + * increasing and positive + * @return - pointer to a new distribution or null pointer if parameters are + * wrong or memory allocation fails + */ +distribution_t *distribution_new_custom(size_t array_size, + double *custom_buckets_boundaries); + +/** add new value to a distribution **/ +void distribution_update(distribution_t *dist, double gauge); + +/** + * @param percent - should be in (0; 100] range + * @return - an approximation of percent percentile + * (upper bound of such bucket that all less or equal buckets contain more than + * percent percents of values) or NAN if parameters are wrong or distribution is + * empty + */ +double distribution_percentile(distribution_t *dist, double percent); + +/** @return - average of all values in distribution or NAN if distribution is + * empty */ +double distribution_average(distribution_t *dist); + +/** @return - pointer to the copy of distribution or null if memory allocation + * fails */ +distribution_t *distribution_clone(distribution_t *dist); + +/** destroy the distribution and free memory **/ +void distribution_destroy(distribution_t *d); + +/** @return - number of buckets stored in the distribution **/ +size_t distribution_num_buckets(distribution_t *dist); + +/** @return - array of buckets in the distribution **/ +buckets_array_t get_buckets(distribution_t *dist); + +void destroy_buckets_array(buckets_array_t buckets_array); + +#endif // COLLECTD_DISTRIBUTION_H diff --git a/src/daemon/distribution_test.c b/src/daemon/distribution_test.c new file mode 100644 index 0000000000..4d29c000cd --- /dev/null +++ b/src/daemon/distribution_test.c @@ -0,0 +1,486 @@ +/** + * collectd - src/daemon/distribution_test.c + * Copyright (C) 2020 Google LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * Authors: + * Svetlana Shmidt + */ + +#include "collectd.h" + +#include "distribution.h" +#include "testing.h" + +static double *linear_upper_bounds(size_t num, double size) { + double *linear_upper_bounds = calloc(num, sizeof(*linear_upper_bounds)); + for (size_t i = 0; i + 1 < num; i++) + linear_upper_bounds[i] = (i + 1) * size; + linear_upper_bounds[num - 1] = INFINITY; + return linear_upper_bounds; +} + +static double *exponential_upper_bounds(size_t num, double base, + double factor) { + double *exponential_upper_bounds = + calloc(num, sizeof(*exponential_upper_bounds)); + exponential_upper_bounds[0] = factor; + for (size_t i = 1; i + 1 < num; i++) + exponential_upper_bounds[i] = + factor * pow(base, i); // exponential_upper_bounds[i - 1] * base; + exponential_upper_bounds[num - 1] = INFINITY; + return exponential_upper_bounds; +} + +DEF_TEST(distribution_new_linear) { + struct { + size_t num_buckets; + double size; + double *want_get; + int want_err; + } cases[] = { + { + .num_buckets = 0, + .size = 5, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .num_buckets = 3, + .size = -5, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .num_buckets = 5, + .size = 0, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .num_buckets = 3, + .size = 2.5, + .want_get = linear_upper_bounds(3, 2.5), + }, + { + .num_buckets = 5, + .size = 5.75, + .want_get = linear_upper_bounds(5, 5.75), + }, + { + .num_buckets = 151, + .size = 0.7, + .want_get = linear_upper_bounds(151, 0.7), + }, + { + .num_buckets = 111, + .size = 1074, + .want_get = linear_upper_bounds(111, 1074), + }, + { + .num_buckets = 77, + .size = 1.0 / 3.0, + .want_get = linear_upper_bounds(77, 1.0 / 3.0), + }, + { + .num_buckets = 1, + .size = 100, + .want_get = linear_upper_bounds(1, 100), + }, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + printf("## Case %zu:\n", i); + if (cases[i].want_err != 0) { + EXPECT_EQ_PTR( + cases[i].want_get, + distribution_new_linear(cases[i].num_buckets, cases[i].size)); + EXPECT_EQ_INT(cases[i].want_err, errno); + continue; + } + distribution_t *d; + CHECK_NOT_NULL( + d = distribution_new_linear(cases[i].num_buckets, cases[i].size)); + buckets_array_t buckets_array = get_buckets(d); + for (size_t j = 0; j < cases[i].num_buckets; j++) { + EXPECT_EQ_DOUBLE(cases[i].want_get[j], buckets_array.buckets[j].maximum); + } + destroy_buckets_array(buckets_array); + distribution_destroy(d); + } + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + free(cases[i].want_get); + } + return 0; +} + +DEF_TEST(distribution_new_exponential) { + struct { + size_t num_buckets; + double factor; + double base; + double *want_get; + int want_err; + } cases[] = { + { + .num_buckets = 0, + .factor = 5, + .base = 8, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .num_buckets = 5, + .factor = 0.2, + .base = -1, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .num_buckets = 8, + .factor = 100, + .base = 0.5, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .num_buckets = 100, + .factor = 5.87, + .base = 1, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .num_buckets = 6, + .factor = 0, + .base = 9.005, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .num_buckets = 16, + .factor = -153, + .base = 1.41, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .num_buckets = 1, + .factor = 10, + .base = 1.05, + .want_get = exponential_upper_bounds(1, 10, 1.05), + }, + { + .num_buckets = 63, + .factor = 1, + .base = 2, + .want_get = exponential_upper_bounds(63, 2, 1), + }, + { + .num_buckets = 600, + .factor = 0.55, + .base = 1.055, + .want_get = exponential_upper_bounds(600, 1.055, 0.55), + }, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + printf("## Case %zu:\n", i); + if (cases[i].want_err != 0) { + EXPECT_EQ_PTR(cases[i].want_get, + distribution_new_exponential( + cases[i].num_buckets, cases[i].base, cases[i].factor)); + EXPECT_EQ_INT(cases[i].want_err, errno); + continue; + } + distribution_t *d; + CHECK_NOT_NULL(d = distribution_new_exponential( + cases[i].num_buckets, cases[i].base, cases[i].factor)); + buckets_array_t buckets_array = get_buckets(d); + for (size_t j = 0; j < cases[i].num_buckets; j++) { + EXPECT_EQ_DOUBLE(cases[i].want_get[j], buckets_array.buckets[j].maximum); + } + destroy_buckets_array(buckets_array); + distribution_destroy(d); + } + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + free(cases[i].want_get); + } + return 0; +} + +DEF_TEST(distribution_new_custom) { + struct { + size_t array_size; + double *custom_boundaries; + double *want_get; + int want_err; + } cases[] = { + { + .array_size = 0, + .want_get = (double[]){INFINITY}, + }, + { + .array_size = 5, + .custom_boundaries = (double[]){0, 1, 2, 3, 4}, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .array_size = 3, + .custom_boundaries = (double[]){-5, 7, 3}, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .array_size = 4, + .custom_boundaries = (double[]){5.7, 6.0, 6.0, 7.0}, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .array_size = 1, + .custom_boundaries = (double[]){105.055}, + .want_get = (double[]){105.055, INFINITY}, + }, + { + .array_size = 5, + .custom_boundaries = (double[]){8, 100, 1000, 1008, INFINITY}, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .array_size = 7, + .custom_boundaries = (double[]){2, 4, 8, 6, 2, 16, 77.5}, + .want_get = NULL, + .want_err = EINVAL, + }, + { + .array_size = 10, + .custom_boundaries = (double[]){77.5, 100.203, 122.01, 137.23, 200, + 205, 210, 220, 230, 256}, + .want_get = (double[]){77.5, 100.203, 122.01, 137.23, 200, 205, 210, + 220, 230, 256, INFINITY}, + }, + }; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + if (cases[i].want_err != 0) { + EXPECT_EQ_PTR(cases[i].want_get, + distribution_new_custom(cases[i].array_size, + cases[i].custom_boundaries)); + EXPECT_EQ_INT(cases[i].want_err, errno); + continue; + } + distribution_t *d; + CHECK_NOT_NULL(d = distribution_new_custom(cases[i].array_size, + cases[i].custom_boundaries)); + buckets_array_t buckets_array = get_buckets(d); + for (size_t j = 0; j < cases[i].array_size + 1; j++) { + EXPECT_EQ_DOUBLE(cases[i].want_get[j], buckets_array.buckets[j].maximum); + } + destroy_buckets_array(buckets_array); + distribution_destroy(d); + } + return 0; +} + +DEF_TEST(update) { + struct { + distribution_t *dist; + size_t num_gauges; + double *gauges; + uint64_t *want_counters; + int want_err; + } cases[] = { + { + .dist = distribution_new_linear(6, 5), + .num_gauges = 10, + .gauges = (double[]){25, 30, 5, 7, 11, 10.5, 8.03, 1112.4, 35, 12.7}, + .want_counters = (uint64_t[]){0, 3, 3, 0, 0, 4}, + }, + { + .dist = distribution_new_exponential(4, 1.41, 1), + .num_gauges = 0, + .gauges = NULL, + .want_counters = (uint64_t[]){0, 0, 0, 0}, + }, + { + .dist = distribution_new_exponential(5, 2, 3), + .num_gauges = 5, + .gauges = (double[]){1, 7, 3, 10, 77}, + .want_counters = (uint64_t[]){1, 1, 2, 0, 1}, + }, + { + .dist = distribution_new_linear(100, 22), + .num_gauges = 3, + .gauges = (double[]){1000, 2, -8}, + .want_err = EINVAL, + }, + { + .dist = distribution_new_custom(3, (double[]){5, 20, 35}), + .num_gauges = 7, + .gauges = (double[]){7.05, 22.37, 40.83, 90.55, 12.34, 14.2, 6.0}, + .want_counters = (uint64_t[]){0, 4, 1, 2}, + }, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + for (size_t j = 0; j < cases[i].num_gauges; j++) { + distribution_update(cases[i].dist, cases[i].gauges[j]); + } + if (cases[i].want_err != 0) { + EXPECT_EQ_INT(cases[i].want_err, errno); + distribution_destroy(cases[i].dist); + continue; + } + buckets_array_t buckets_array = get_buckets(cases[i].dist); + for (size_t j = 0; j < buckets_array.num_buckets; j++) { + EXPECT_EQ_INT(cases[i].want_counters[j], + buckets_array.buckets[j].bucket_counter); + } + destroy_buckets_array(buckets_array); + distribution_destroy(cases[i].dist); + } + return 0; +} + +DEF_TEST(average) { + struct { + distribution_t *dist; + size_t num_gauges; + double *update_gauges; + double want_average; + } cases[] = { + { + .dist = distribution_new_linear(6, 2), + .num_gauges = 0, + .want_average = NAN, + }, + { + .dist = distribution_new_linear(7, 10), + .num_gauges = 5, + .update_gauges = (double[]){3, 2, 5.7, 22.3, 7.5}, + .want_average = 40.5 / 5.0, + }, + { + .dist = distribution_new_exponential(10, 2, 0.75), + .num_gauges = 8, + .update_gauges = (double[]){2, 4, 6, 8, 22, 11, 77, 1005}, + .want_average = 1135.0 / 8.0, + }, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + for (size_t j = 0; j < cases[i].num_gauges; j++) { + distribution_update(cases[i].dist, cases[i].update_gauges[j]); + } + EXPECT_EQ_DOUBLE(cases[i].want_average, + distribution_average(cases[i].dist)); + distribution_destroy(cases[i].dist); + } + return 0; +} + +DEF_TEST(percentile) { + struct { + distribution_t *dist; + size_t num_gauges; + double *update_gauges; + double percent; + double want_percentile; + int want_err; + } cases[] = { + { + .dist = distribution_new_linear(5, 7), + .num_gauges = 1, + .update_gauges = (double[]){4}, + .percent = 105, + .want_percentile = NAN, + .want_err = EINVAL, + }, + { + .dist = distribution_new_linear(8, 10), + .num_gauges = 0, + .percent = 20, + .want_percentile = NAN, + }, + { + .dist = distribution_new_exponential(5, 2, 0.2), + .num_gauges = 2, + .update_gauges = (double[]){4, 30.08}, + .percent = -5, + .want_percentile = NAN, + .want_err = EINVAL, + }, + { + .dist = distribution_new_exponential(10, 2, 0.75), + .num_gauges = 8, + .update_gauges = (double[]){2, 4, 6, 8, 22, 11, 77, 1005}, + .percent = 50, + .want_percentile = 12, + }, + { + .dist = distribution_new_custom(3, (double[]){5, 20, 35}), + .num_gauges = 7, + .update_gauges = (double[]){5.5, 10.5, 11.3, 6.7, 24.7, 40.05, 35}, + .percent = 4.0 / 7.0 * 100, + .want_percentile = 20, + }, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + for (size_t j = 0; j < cases[i].num_gauges; j++) { + distribution_update(cases[i].dist, cases[i].update_gauges[j]); + } + EXPECT_EQ_DOUBLE(cases[i].want_percentile, + distribution_percentile(cases[i].dist, cases[i].percent)); + if (cases[i].want_err != 0) + EXPECT_EQ_INT(cases[i].want_err, errno); + distribution_destroy(cases[i].dist); + } + return 0; +} + +DEF_TEST(clone) { + distribution_t *dist = distribution_new_linear(5, 7); + distribution_update(dist, 5); + distribution_update(dist, 7); + distribution_t *clone = distribution_clone(dist); + EXPECT_EQ_INT(distribution_num_buckets(clone), 5); + EXPECT_EQ_DOUBLE(distribution_percentile(clone, 50), 7); + distribution_update(dist, 10); + EXPECT_EQ_DOUBLE(distribution_percentile(clone, 50), 7); + EXPECT_EQ_DOUBLE(distribution_percentile(dist, 50), 14); + distribution_update(clone, 2); + EXPECT_EQ_DOUBLE(distribution_percentile(dist, 50), 14); + distribution_destroy(dist); + distribution_update(clone, 25); + distribution_update(clone, 200); + EXPECT_EQ_DOUBLE(distribution_percentile(clone, 80), 28); + distribution_destroy(clone); + return 0; +} + +int main() { + RUN_TEST(distribution_new_linear); + RUN_TEST(distribution_new_exponential); + RUN_TEST(distribution_new_custom); + RUN_TEST(update); + RUN_TEST(average); + RUN_TEST(percentile); + RUN_TEST(clone); + END_TEST; +}