| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| #include <pthread.h> |
| #include <unistd.h> // usleep |
| #include <sched.h> // sched_yield |
| #include <stdio.h> // snprintf |
| #include <string.h> // memset |
| #include <algorithm> // std::max |
| #include <limits> |
| #include <map> |
| #include <memory> // std::make_shared |
| #include <sstream> |
| #include <string> |
| #include <vector> |
| #include <gtest/gtest.h> |
| #include <butil/atomicops.h> |
| #include <butil/float_util.h> |
| #include <butil/strings/string_number_conversions.h> |
| #include <butil/time.h> |
| #include "bvar/bvar.h" |
| #include "bvar/detail/combiner.h" |
| #include "bvar/histogram.h" |
| #include "bvar/multi_dimension.h" |
| #include "bvar/window.h" |
| |
| namespace { |
| |
| // Test builds use -fno-access-control (see test/CMakeLists.txt), so the tests |
| // below call private members such as Histogram::get_value() directly. |
| class HistogramTest : public testing::Test {}; |
| |
| // The element container of a Histogram::Value, which is the generic mutex one: |
| // the value is far too wide to be atomical. |
| TEST_F(HistogramTest, element_container) { |
| ASSERT_FALSE(bvar::detail::is_atomical<bvar::Histogram::Value>::value); |
| bvar::Histogram::BucketSchema schema({10, 20, 30}); |
| bvar::detail::ElementContainer<bvar::Histogram::Value> c; |
| bvar::Histogram::Value v; |
| // Freshly constructed, before any store(). |
| c.load(&v); |
| ASSERT_EQ(0, v.num); |
| ASSERT_DOUBLE_EQ(0.0, v.sum); |
| |
| c.store(bvar::Histogram::Value(schema.num_buckets())); |
| c.modify(bvar::detail::AddSampleToHistogram(&schema), 5.25); |
| c.modify(bvar::detail::AddSampleToHistogram(&schema), 25.5); |
| c.modify(bvar::detail::AddSampleToHistogram(&schema), 1000.75); |
| c.load(&v); |
| ASSERT_EQ(3, v.num); |
| ASSERT_DOUBLE_EQ(1031.5, v.sum); |
| ASSERT_EQ(4u, v.num_buckets); |
| ASSERT_EQ(1u, v.counts[0]); // 5.25 -> (-inf, 10] |
| ASSERT_EQ(0u, v.counts[1]); |
| ASSERT_EQ(1u, v.counts[2]); // 25.5 -> (20, 30] |
| ASSERT_EQ(1u, v.counts[3]); // 1000.75 -> +Inf |
| |
| // store() publishes a whole value, overwriting everything. |
| c.store(bvar::Histogram::Value(schema.num_buckets())); |
| c.load(&v); |
| ASSERT_EQ(0, v.num); |
| ASSERT_EQ(0u, v.counts[3]); |
| } |
| |
| // Fixed workload for export and performance tests, not a library default. |
| static bvar::Histogram::BucketSchema test_latency_schema() { |
| return {10, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, |
| 10240, 20480, 40960, 81920, 163840, 327680, 655360, |
| 1310720, 2621440, 5242880}; |
| } |
| |
| TEST_F(HistogramTest, schema_fractional_bounds) { |
| bvar::Histogram::BucketSchema schema = {-0.5, 0.25, 1.5}; |
| ASSERT_EQ(std::vector<double>({-0.5, 0.25, 1.5}), schema.bounds()); |
| ASSERT_EQ(0u, schema.index_of(-0.5)); |
| ASSERT_EQ(1u, schema.index_of(-0.25)); |
| ASSERT_EQ(1u, schema.index_of(0.25)); |
| ASSERT_EQ(2u, schema.index_of(1.5)); |
| ASSERT_EQ(3u, schema.index_of(1.5001)); |
| } |
| |
| // Bounds the caller spells out, through either entry point. |
| TEST_F(HistogramTest, schema_custom_bounds) { |
| double expected[] = {0.125, 1.25, 8.5, 64.5}; |
| |
| // As an initializer_list. |
| bvar::Histogram::BucketSchema from_list = {0.125, 1.25, 8.5, 64.5}; |
| ASSERT_EQ(4u, from_list.num_bounds()); |
| ASSERT_EQ(5u, from_list.num_buckets()); |
| for (size_t i = 0; i < from_list.num_bounds(); ++i) { |
| ASSERT_EQ(expected[i], from_list.bound_at(i)) << "i=" << i; |
| } |
| |
| // As a vector. |
| std::vector<double> v(expected, expected + arraysize(expected)); |
| bvar::Histogram::BucketSchema from_vector(v); |
| ASSERT_EQ(v, from_vector.bounds()); |
| |
| // The initializer_list ctor is implicit, so the bounds can be written at |
| // the declaration of the histogram itself. |
| bvar::Histogram h("hist_custom_bounds_test", {0.125, 1.25, 8.5, 64.5}); |
| ASSERT_EQ(v, h.schema().bounds()); |
| h << 0.1 << 0.5 << 100.25; |
| bvar::Histogram::Value hv = h.get_value(); |
| ASSERT_EQ(3, hv.num); |
| ASSERT_EQ(5u, hv.num_buckets); |
| ASSERT_EQ(1u, hv.counts[0]); // 0.1 <= 0.125 |
| ASSERT_EQ(1u, hv.counts[1]); // 0.125 < 0.5 <= 1.25 |
| ASSERT_EQ(0u, hv.counts[2]); |
| ASSERT_EQ(0u, hv.counts[3]); |
| ASSERT_EQ(1u, hv.counts[4]); // 100.25 > 64.5, the +Inf bucket |
| } |
| |
| // The repairs apply no matter which entry point the bounds came through. |
| TEST_F(HistogramTest, schema_custom_bounds_are_validated) { |
| bvar::Histogram::BucketSchema unsorted = {10, 5, 10, 20}; |
| ASSERT_EQ(2u, unsorted.num_bounds()); |
| ASSERT_EQ(10, unsorted.bound_at(0)); |
| ASSERT_EQ(20, unsorted.bound_at(1)); |
| |
| bvar::Histogram::BucketSchema empty_list = {}; |
| ASSERT_EQ(1u, empty_list.num_bounds()); |
| |
| bvar::Histogram::BucketSchema empty_vector{std::vector<double>()}; |
| ASSERT_EQ(1u, empty_vector.num_bounds()); |
| |
| std::vector<double> too_many; |
| for (size_t i = 0; i < bvar::MAX_HISTOGRAM_BUCKETS + 10; ++i) { |
| too_many.push_back((double)i + 1); |
| } |
| ASSERT_EQ(bvar::MAX_HISTOGRAM_BUCKETS - 1, |
| bvar::Histogram::BucketSchema(too_many).num_bounds()); |
| |
| std::vector<double> non_finite = { |
| -std::numeric_limits<double>::infinity(), 1.5, |
| std::numeric_limits<double>::quiet_NaN(), 2.5, |
| std::numeric_limits<double>::infinity()}; |
| bvar::Histogram::BucketSchema finite_only(non_finite); |
| ASSERT_EQ(std::vector<double>({1.5, 2.5}), finite_only.bounds()); |
| } |
| |
| TEST_F(HistogramTest, index_of_is_le) { |
| // Buckets are (-inf,0.5], (0.5,1.5], (1.5,3], (3,+inf) |
| bvar::Histogram::BucketSchema s = bvar::Histogram::BucketSchema({0.5, 1.5, 3.0}); |
| ASSERT_EQ(0u, s.index_of(-100)); |
| ASSERT_EQ(0u, s.index_of(0)); |
| ASSERT_EQ(0u, s.index_of(0.5)); // le, so 0.5 is in the first bucket |
| ASSERT_EQ(1u, s.index_of(0.5001)); |
| ASSERT_EQ(1u, s.index_of(1.5)); |
| ASSERT_EQ(2u, s.index_of(1.5001)); |
| ASSERT_EQ(2u, s.index_of(3.0)); |
| ASSERT_EQ(3u, s.index_of(3.0001)); // the +Inf bucket |
| ASSERT_EQ(3u, s.index_of(1000000)); |
| } |
| |
| TEST_F(HistogramTest, add_and_get_value) { |
| bvar::Histogram h(bvar::Histogram::BucketSchema({0.5, 1.5, 3.0})); |
| ASSERT_TRUE(h.valid()); |
| ASSERT_EQ(0, h.count()); |
| ASSERT_DOUBLE_EQ(0.0, h.sum()); |
| ASSERT_DOUBLE_EQ(0.0, h.average()); |
| |
| h << 0.25 << 1.25 << 2.5 << 3.5 << 1.25; |
| |
| bvar::Histogram::Value v = h.get_value(); |
| ASSERT_EQ(5, v.num); |
| ASSERT_DOUBLE_EQ(8.75, v.sum); |
| ASSERT_EQ(4u, v.num_buckets); |
| ASSERT_EQ(1u, v.counts[0]); // 0.25 |
| ASSERT_EQ(2u, v.counts[1]); // 1.25, 1.25 |
| ASSERT_EQ(1u, v.counts[2]); // 2.5 |
| ASSERT_EQ(1u, v.counts[3]); // 3.5 -> +Inf |
| ASSERT_EQ(5, h.count()); |
| ASSERT_DOUBLE_EQ(8.75, h.sum()); |
| ASSERT_DOUBLE_EQ(1.75, h.average()); |
| } |
| |
| TEST_F(HistogramTest, value_arithmetic) { |
| bvar::Histogram::Value a(4); |
| a.add(0, 0.5); |
| a.add(1, 1.25); |
| bvar::Histogram::Value b(4); |
| b.add(1, 0.75); |
| b.add(3, 2.5); |
| |
| bvar::Histogram::Value c = a; |
| c += b; |
| ASSERT_EQ(4, c.num); |
| ASSERT_DOUBLE_EQ(5.0, c.sum); |
| ASSERT_EQ(1u, c.counts[0]); |
| ASSERT_EQ(2u, c.counts[1]); |
| ASSERT_EQ(0u, c.counts[2]); |
| ASSERT_EQ(1u, c.counts[3]); |
| |
| // These values are exactly representable, so -= restores the original. |
| // General Window sums may still have normal floating-point rounding error. |
| c -= b; |
| ASSERT_EQ(a.num, c.num); |
| ASSERT_DOUBLE_EQ(a.sum, c.sum); |
| for (size_t i = 0; i < bvar::MAX_HISTOGRAM_BUCKETS; ++i) { |
| ASSERT_EQ(a.counts[i], c.counts[i]) << "i=" << i; |
| } |
| } |
| |
| TEST_F(HistogramTest, floating_point_sum_and_window_difference) { |
| ASSERT_TRUE(std::is_trivially_copyable<bvar::Histogram::Value>::value); |
| bvar::Histogram h({0}); |
| h << -0.5 << 0.25; |
| bvar::Histogram::Value before = h.get_value(); |
| h << 1.125; |
| bvar::Histogram::Value delta = h.get_value(); |
| delta -= before; |
| ASSERT_EQ(1, delta.num); |
| ASSERT_DOUBLE_EQ(1.125, delta.sum); |
| ASSERT_DOUBLE_EQ(1.125, delta.get_average_double()); |
| ASSERT_DOUBLE_EQ(0.875, h.sum()); |
| } |
| |
| TEST_F(HistogramTest, ignores_non_finite_observations) { |
| bvar::Histogram h({0}); |
| h << std::numeric_limits<double>::quiet_NaN() |
| << std::numeric_limits<double>::infinity() |
| << -std::numeric_limits<double>::infinity() |
| << 0.25; |
| ASSERT_EQ(1, h.count()); |
| ASSERT_DOUBLE_EQ(0.25, h.sum()); |
| } |
| |
| TEST_F(HistogramTest, describe) { |
| bvar::Histogram h(bvar::Histogram::BucketSchema({0.5, 1.5, 3.25})); |
| h << 0.25 << 1.25 << 1.25 << 3.5; |
| std::ostringstream os; |
| h.describe(os, false); |
| ASSERT_EQ("{\"count\":4,\"sum\":6.25,\"bounds\":[0.5,1.5,3.25]," |
| "\"counts\":[1,2,0,1]}", os.str()); |
| |
| // A Histogram::Value on its own has no bounds to print. |
| std::ostringstream os2; |
| os2 << h.get_value(); |
| ASSERT_EQ("{\"count\":4,\"sum\":6.25,\"counts\":[1,2,0,1]}", os2.str()); |
| } |
| |
| TEST_F(HistogramTest, describe_writes_a_non_finite_sum_as_null) { |
| double big = std::numeric_limits<double>::max(); |
| bvar::Histogram h(bvar::Histogram::BucketSchema({0.5, 1.5})); |
| h << big << big; |
| ASSERT_FALSE(butil::IsFinite(h.get_value().sum)); |
| |
| std::ostringstream os; |
| h.describe(os, false); |
| ASSERT_EQ("{\"count\":2,\"sum\":null,\"bounds\":[0.5,1.5]," |
| "\"counts\":[0,0,2]}", os.str()); |
| |
| std::ostringstream os2; |
| os2 << h.get_value(); |
| ASSERT_EQ("{\"count\":2,\"sum\":null,\"counts\":[0,0,2]}", os2.str()); |
| } |
| |
| // Collects everything a Dumper is asked to write, in order. |
| class RecordingDumper : public bvar::Dumper { |
| public: |
| bool dump(const std::string& name, |
| const butil::StringPiece& desc) override { |
| lines.push_back("dump_" + name + " " + desc.as_string()); |
| return true; |
| } |
| bool dump_mvar(const std::string& name, |
| const butil::StringPiece& desc) override { |
| lines.push_back("mvar_" + name + " " + desc.as_string()); |
| return true; |
| } |
| bool dump_comment(const std::string& name, |
| const std::string& type) override { |
| lines.push_back("comment " + name + " " + type); |
| return true; |
| } |
| std::vector<std::string> lines; |
| }; |
| |
| TEST_F(HistogramTest, dump) { |
| bvar::Histogram h(bvar::Histogram::BucketSchema({0.5, 1.5, 3.25})); |
| h << 0.25 << 1.25 << 1.25 << 3.5; |
| |
| RecordingDumper d; |
| bvar::DumpOptions opt; |
| ASSERT_TRUE(h.dump(&d, opt, "foo")); |
| |
| ASSERT_EQ(7u, d.lines.size()); |
| ASSERT_EQ("comment foo histogram", d.lines[0]); |
| // The counts a histogram dumps are cumulative: 1, 1+2, 1+2+0, 1+2+0+1. |
| ASSERT_EQ("mvar_foo_bucket{le=\"0.5\"} 1", d.lines[1]); |
| ASSERT_EQ("mvar_foo_bucket{le=\"1.5\"} 3", d.lines[2]); |
| ASSERT_EQ("mvar_foo_bucket{le=\"3.25\"} 3", d.lines[3]); |
| ASSERT_EQ("mvar_foo_bucket{le=\"+Inf\"} 4", d.lines[4]); |
| ASSERT_EQ("mvar_foo_sum 6.25", d.lines[5]); |
| ASSERT_EQ("mvar_foo_count 4", d.lines[6]); |
| } |
| |
| // A Dumper that stops partway must stop the histogram too. |
| class FailingDumper : public bvar::Dumper { |
| public: |
| explicit FailingDumper(int fail_at) : _fail_at(fail_at), _n(0) {} |
| bool dump(const std::string&, const butil::StringPiece&) override { |
| return _n++ != _fail_at; |
| } |
| bool dump_mvar(const std::string&, const butil::StringPiece&) override { |
| return _n++ != _fail_at; |
| } |
| bool dump_comment(const std::string&, const std::string&) override { |
| return _n++ != _fail_at; |
| } |
| int count() const { return _n; } |
| private: |
| int _fail_at; |
| int _n; |
| }; |
| |
| TEST_F(HistogramTest, dump_stops_on_failure) { |
| bvar::Histogram h(bvar::Histogram::BucketSchema({10, 20, 30})); |
| h << 5; |
| bvar::DumpOptions opt; |
| // Fail on the comment, on a bucket, on _sum and on _count in turn. |
| int fail_points[] = {0, 2, 5, 6}; |
| for (size_t i = 0; i < arraysize(fail_points); ++i) { |
| FailingDumper d(fail_points[i]); |
| ASSERT_FALSE(h.dump(&d, opt, "foo")) << "fail_at=" << fail_points[i]; |
| ASSERT_EQ(fail_points[i] + 1, d.count()) << "fail_at=" << fail_points[i]; |
| } |
| } |
| |
| // Variables that don't override dump() must keep dumping as one metric. |
| TEST_F(HistogramTest, default_variable_dump_is_unchanged) { |
| bvar::Adder<int> a; |
| a << 7; |
| RecordingDumper d; |
| bvar::DumpOptions opt; |
| ASSERT_TRUE(a.dump(&d, opt, "bar")); |
| ASSERT_EQ(1u, d.lines.size()); |
| ASSERT_EQ("dump_bar 7", d.lines[0]); |
| } |
| |
| // A Dumper written before composite metrics existed only implements dump(). |
| // The samples of a histogram carry their labels inside the name, which such a |
| // dumper has no way of reading, so it must not start receiving them behind its |
| // back: opting in is what the dump_mvar() override is for. |
| class DumpOnlyDumper : public bvar::Dumper { |
| public: |
| bool dump(const std::string& name, |
| const butil::StringPiece& desc) override { |
| lines.push_back(name + " " + desc.as_string()); |
| return true; |
| } |
| std::vector<std::string> lines; |
| }; |
| |
| TEST_F(HistogramTest, samples_reach_an_opted_in_dumper_only) { |
| bvar::Histogram h(bvar::Histogram::BucketSchema({10, 20})); |
| h << 5 << 25; |
| |
| DumpOnlyDumper d; |
| bvar::DumpOptions opt; |
| ASSERT_TRUE(h.dump(&d, opt, "foo")); |
| ASSERT_TRUE(d.lines.empty()) << d.lines[0]; |
| |
| // A plain value is unaffected, it goes through dump() as it always did. |
| bvar::Adder<int> a; |
| a << 7; |
| ASSERT_TRUE(a.dump(&d, opt, "bar")); |
| ASSERT_EQ(1u, d.lines.size()); |
| ASSERT_EQ("bar 7", d.lines[0]); |
| } |
| |
| TEST_F(HistogramTest, non_finite_sum_uses_prometheus_spelling) { |
| double big = std::numeric_limits<double>::max(); |
| { |
| bvar::Histogram h(bvar::Histogram::BucketSchema({10, 20})); |
| h << big << big; |
| ASSERT_FALSE(butil::IsFinite(h.get_value().sum)); |
| |
| RecordingDumper d; |
| ASSERT_TRUE(h.dump_samples(&d, 0, "foo", butil::StringPiece())); |
| ASSERT_EQ(5u, d.lines.size()); |
| ASSERT_EQ("mvar_foo_sum +Inf", d.lines[3]); |
| // The count is untouched, both values were recorded. |
| ASSERT_EQ("mvar_foo_count 2", d.lines[4]); |
| } |
| { |
| bvar::Histogram h(bvar::Histogram::BucketSchema({10, 20})); |
| h << -big << -big; |
| RecordingDumper d; |
| ASSERT_TRUE(h.dump_samples(&d, 0, "foo", butil::StringPiece())); |
| ASSERT_EQ("mvar_foo_sum -Inf", d.lines[3]); |
| } |
| } |
| |
| TEST_F(HistogramTest, dump_exposed_goes_through_the_virtual) { |
| bvar::Histogram h("hist_dump_exposed_test", |
| bvar::Histogram::BucketSchema({10, 20})); |
| h << 5 << 25; |
| |
| RecordingDumper d; |
| bvar::DumpOptions opt; |
| opt.white_wildcards = "hist_dump_exposed_test"; |
| ASSERT_EQ(1, bvar::Variable::dump_exposed(&d, &opt)); |
| ASSERT_EQ(6u, d.lines.size()); |
| ASSERT_EQ("comment hist_dump_exposed_test histogram", d.lines[0]); |
| ASSERT_EQ("mvar_hist_dump_exposed_test_bucket{le=\"10\"} 1", d.lines[1]); |
| ASSERT_EQ("mvar_hist_dump_exposed_test_bucket{le=\"20\"} 1", d.lines[2]); |
| ASSERT_EQ("mvar_hist_dump_exposed_test_bucket{le=\"+Inf\"} 2", d.lines[3]); |
| ASSERT_EQ("mvar_hist_dump_exposed_test_sum 30", d.lines[4]); |
| ASSERT_EQ("mvar_hist_dump_exposed_test_count 2", d.lines[5]); |
| } |
| |
| TEST_F(HistogramTest, expose_and_describe_exposed) { |
| bvar::Histogram h({10, 20}); |
| ASSERT_EQ(0, h.expose("hist_expose_test")); |
| ASSERT_EQ("hist_expose_test", h.name()); |
| h << 100; |
| ASSERT_EQ(h.get_description(), |
| bvar::Variable::describe_exposed("hist_expose_test")); |
| // A distribution can't be plotted as a series. |
| std::ostringstream os; |
| bvar::SeriesOptions so; |
| ASSERT_EQ(1, h.describe_series(os, so)); |
| } |
| |
| TEST_F(HistogramTest, requires_explicit_schema) { |
| static_assert(!std::is_default_constructible<bvar::Histogram>::value, |
| "Histogram requires explicit bucket bounds"); |
| typedef bvar::MultiDimension<bvar::Histogram> Multi; |
| typedef Multi::key_type Keys; |
| typedef butil::StringPiece Name; |
| static_assert(!std::is_constructible<Multi, const Keys&>::value, "no schema"); |
| static_assert(!std::is_constructible<Multi, Name, const Keys&>::value, "no schema"); |
| static_assert(!std::is_constructible<Multi, Name, Name, const Keys&>::value, |
| "no schema"); |
| static_assert(std::is_constructible<Multi, const Keys&, bvar::Histogram::BucketSchema>::value, |
| "explicit schema"); |
| static_assert(std::is_constructible<Multi, Name, const Keys&, |
| bvar::Histogram::BucketSchema>::value, "explicit schema"); |
| static_assert(std::is_constructible<Multi, Name, Name, const Keys&, |
| bvar::Histogram::BucketSchema>::value, "explicit schema"); |
| static_assert(!std::is_constructible<Multi, Name, const Keys&, int>::value, |
| "invalid schema argument"); |
| } |
| |
| TEST_F(HistogramTest, multi_dimension) { |
| bvar::MultiDimension<bvar::Histogram> mhist("hist_mvar_test", |
| {"method", "status"}, |
| test_latency_schema()); |
| bvar::Histogram* h = mhist.get_stats({"echo", "200"}); |
| ASSERT_TRUE(h != nullptr); |
| *h << 15; |
| ASSERT_EQ(1, h->count()); |
| ASSERT_EQ(1u, mhist.count_stats()); |
| } |
| |
| // A Histogram is one family, whatever the number of series inside it. |
| TEST_F(HistogramTest, list_metric_families) { |
| std::vector<bvar::MetricFamily> families = bvar::Histogram::list_metric_families(); |
| ASSERT_EQ(1u, families.size()); |
| ASSERT_STREQ("", families[0].suffix); |
| ASSERT_STREQ("histogram", families[0].type); |
| ASSERT_EQ(std::vector<std::string>({"le"}), families[0].reserved_labels); |
| |
| families = bvar::LatencyRecorder::list_metric_families(); |
| ASSERT_EQ(5u, families.size()); |
| ASSERT_EQ(std::vector<std::string>({"quantile"}), |
| families[0].reserved_labels); |
| ASSERT_TRUE(families[1].reserved_labels.empty()); |
| ASSERT_TRUE(families[2].reserved_labels.empty()); |
| ASSERT_TRUE(families[3].reserved_labels.empty()); |
| ASSERT_TRUE(families[4].reserved_labels.empty()); |
| } |
| |
| // The labels of the enclosing MultiDimension share the brace group with `le`. |
| TEST_F(HistogramTest, dump_samples) { |
| bvar::Histogram h(bvar::Histogram::BucketSchema({10, 20})); |
| h << 5 << 25; |
| |
| RecordingDumper d; |
| ASSERT_TRUE(h.dump_samples(&d, 0, "foo", "method=\"echo\"")); |
| |
| // No comment line: the caller owns the TYPE of the whole family. |
| ASSERT_EQ(5u, d.lines.size()); |
| ASSERT_EQ("mvar_foo_bucket{method=\"echo\",le=\"10\"} 1", d.lines[0]); |
| ASSERT_EQ("mvar_foo_bucket{method=\"echo\",le=\"20\"} 1", d.lines[1]); |
| ASSERT_EQ("mvar_foo_bucket{method=\"echo\",le=\"+Inf\"} 2", d.lines[2]); |
| ASSERT_EQ("mvar_foo_sum{method=\"echo\"} 30", d.lines[3]); |
| ASSERT_EQ("mvar_foo_count{method=\"echo\"} 2", d.lines[4]); |
| } |
| |
| TEST_F(HistogramTest, multi_dimension_dump) { |
| const bvar::Histogram::BucketSchema schema = test_latency_schema(); |
| bvar::MultiDimension<bvar::Histogram> mhist("hist_mvar_dump_test", |
| {"method", "status"}, schema); |
| *mhist.get_stats({"echo", "200"}) << 5 << 25; |
| *mhist.get_stats({"echo", "500"}) << 15; |
| |
| // One metric per bucket of the schema, plus _sum and _count. |
| const size_t nmetrics_per_stats = schema.num_buckets() + 2; |
| const size_t nbuckets = schema.num_buckets(); |
| |
| RecordingDumper d; |
| bvar::DumpOptions opt; |
| // The count is metrics, not label sets: every bucket + _sum + _count for |
| // each of the two label sets. Keeping it that way is what makes the cap of |
| // FLAGS_bvar_max_dump_multi_dimension_metric_number mean lines of output. |
| ASSERT_EQ(2u * nmetrics_per_stats, mhist.dump(&d, &opt)); |
| |
| // Exactly one TYPE line for the family, and it comes first. |
| ASSERT_EQ(1u + 2 * nmetrics_per_stats, d.lines.size()); |
| ASSERT_EQ("comment hist_mvar_dump_test histogram", d.lines[0]); |
| |
| // FlatMap gives no order across label sets, but each of them must be one |
| // uninterrupted run of the buckets of the schema + _sum + _count. |
| std::map<std::string, std::vector<std::string> > blocks; |
| for (size_t i = 1; i < d.lines.size(); i += nmetrics_per_stats) { |
| std::string& first = d.lines[i]; |
| std::string labels = |
| first.substr(first.find('{'), first.find(",le=") - first.find('{')); |
| ASSERT_TRUE(blocks.find(labels) == blocks.end()) |
| << "label set " << labels << " is not contiguous"; |
| blocks[labels].assign(d.lines.begin() + i, |
| d.lines.begin() + i + nmetrics_per_stats); |
| } |
| ASSERT_EQ(2u, blocks.size()); |
| |
| std::vector<std::string>& ok = |
| blocks["{method=\"echo\",status=\"200\""]; |
| ASSERT_EQ(nmetrics_per_stats, ok.size()); |
| std::string prefix = |
| "mvar_hist_mvar_dump_test_bucket{method=\"echo\",status=\"200\",le=\""; |
| // The test schema starts at 10 and doubles: 5 is in the first bucket, |
| // 25 in the second one whose bound is 40. |
| ASSERT_EQ(prefix + "10\"} 1", ok[0]); |
| ASSERT_EQ(prefix + "20\"} 1", ok[1]); |
| ASSERT_EQ(prefix + "40\"} 2", ok[2]); |
| // The +Inf bucket closes the run and, being cumulative, equals _count. |
| ASSERT_EQ(prefix + "+Inf\"} 2", ok[nbuckets - 1]); |
| ASSERT_EQ("mvar_hist_mvar_dump_test_sum" |
| "{method=\"echo\",status=\"200\"} 30", ok[nbuckets]); |
| ASSERT_EQ("mvar_hist_mvar_dump_test_count" |
| "{method=\"echo\",status=\"200\"} 2", ok[nbuckets + 1]); |
| |
| std::vector<std::string>& err = |
| blocks["{method=\"echo\",status=\"500\""]; |
| ASSERT_EQ(nmetrics_per_stats, err.size()); |
| ASSERT_EQ("mvar_hist_mvar_dump_test_count" |
| "{method=\"echo\",status=\"500\"} 1", err[nbuckets + 1]); |
| } |
| |
| // An empty MultiDimension<Histogram> must not leave a dangling TYPE line. |
| TEST_F(HistogramTest, multi_dimension_dump_when_empty) { |
| bvar::MultiDimension<bvar::Histogram> mhist("hist_mvar_empty_test", {"method"}, |
| bvar::Histogram::BucketSchema({10, 20})); |
| RecordingDumper d; |
| bvar::DumpOptions opt; |
| ASSERT_EQ(0u, mhist.dump(&d, &opt)); |
| ASSERT_TRUE(d.lines.empty()); |
| } |
| |
| // A false from the dumper stops the whole dump, as it does for a plain |
| // Variable, and the count only covers what the dumper accepted. |
| TEST_F(HistogramTest, multi_dimension_dump_stops_on_failure) { |
| bvar::MultiDimension<bvar::Histogram> mhist("hist_mvar_failing_test", |
| {"method"}, |
| bvar::Histogram::BucketSchema({10, 20})); |
| *mhist.get_stats({"echo"}) << 5; |
| *mhist.get_stats({"write"}) << 5; |
| bvar::DumpOptions opt; |
| |
| // Fail on the TYPE line: no label set is dumped at all. |
| FailingDumper on_comment(0); |
| ASSERT_EQ(0u, mhist.dump(&on_comment, &opt)); |
| ASSERT_EQ(1, on_comment.count()); |
| |
| // Fail on the _sum of whichever label set comes first. The 3 buckets ahead |
| // of it were dumped, and the second label set is not attempted: 11 lines |
| // would have come out had the dump run to the end. |
| FailingDumper on_sum(4); |
| ASSERT_EQ(3u, mhist.dump(&on_sum, &opt)); |
| ASSERT_EQ(5, on_sum.count()); |
| } |
| |
| // The explicit schema reaches every label combination through the value factory. |
| TEST_F(HistogramTest, multi_dimension_with_custom_schema) { |
| bvar::MultiDimension<bvar::Histogram> mhist( |
| "hist_mvar_schema_test", {"method"}, |
| bvar::Histogram::BucketSchema({10, 20})); |
| |
| bvar::Histogram* h = mhist.get_stats({"echo"}); |
| ASSERT_TRUE(h != nullptr); |
| std::vector<double> expected{10, 20}; |
| ASSERT_EQ(expected, h->schema().bounds()); |
| // The schema reaches values created later too, not just the first one. |
| ASSERT_EQ(expected, mhist.get_stats({"write"})->schema().bounds()); |
| |
| *h << 5 << 25; |
| RecordingDumper d; |
| bvar::DumpOptions opt; |
| mhist.dump(&d, &opt); |
| // One TYPE line, then the 3 buckets of the schema + _sum + _count for each |
| // of the two label sets. FlatMap gives no order across them, so look the |
| // line up instead of indexing into the dump. |
| ASSERT_EQ(1u + 2 * 5u, d.lines.size()); |
| ASSERT_NE(d.lines.end(),std::find(d.lines.begin(), d.lines.end(), |
| "mvar_hist_mvar_schema_test_bucket" |
| "{method=\"echo\",le=\"+Inf\"} 2")); |
| } |
| |
| // A series is a list of numbers for /vars to plot. A distribution is not one, |
| // so an exposed Window<Histogram> must not keep a series: nothing could draw it |
| // and it would cost 174 samples of a value this wide. |
| TEST_F(HistogramTest, exposed_window_has_no_series) { |
| ASSERT_FALSE(bvar::detail::HasPlottableSeries<bvar::Histogram::Value>::value); |
| ASSERT_TRUE(bvar::detail::HasPlottableSeries<int64_t>::value); |
| // Otherwise nothing below would keep a series in the first place. |
| ASSERT_TRUE(bvar::FLAGS_save_series); |
| |
| bvar::Histogram h(bvar::Histogram::BucketSchema({10, 20})); |
| bvar::Window<bvar::Histogram> w("hist_window_series_test", &h, 2); |
| std::ostringstream os; |
| bvar::SeriesOptions options; |
| // 1 is what Variable::describe_series() answers when there is no series. |
| ASSERT_EQ(1, bvar::Variable::describe_series_exposed("hist_window_series_test", os, options)); |
| ASSERT_TRUE(os.str().empty()); |
| |
| // A window over a number does keep one, the opt-out is not global. |
| bvar::Adder<int64_t> a; |
| bvar::Window<bvar::Adder<int64_t> > wa("adder_window_series_test", &a, 2); |
| ASSERT_EQ(0, bvar::Variable::describe_series_exposed("adder_window_series_test", os, options)); |
| ASSERT_FALSE(os.str().empty()); |
| } |
| |
| TEST_F(HistogramTest, window) { |
| bvar::Histogram h(bvar::Histogram::BucketSchema({0.5, 1.5, 3.0})); |
| bvar::Window<bvar::Histogram> w(&h, 10); |
| |
| h << 0.25 << 1.25; |
| sleep(1); |
| h << 2.5; |
| // One more second so that get_value() has a sample taken after the last |
| // value, to diff against the construction-time baseline. |
| sleep(1); |
| |
| bvar::Histogram::Value wv = w.get_value(); |
| // Everything recorded, all of it inside the window. |
| ASSERT_EQ(3, wv.num); |
| ASSERT_DOUBLE_EQ(4.0, wv.sum); |
| ASSERT_EQ(1u, wv.counts[0]); |
| ASSERT_EQ(1u, wv.counts[1]); |
| ASSERT_EQ(1u, wv.counts[2]); |
| |
| // Unlike Percentile, the underlying histogram is not reset by sampling: |
| // its op has an inverse, so the window is a difference of two snapshots. |
| ASSERT_EQ(3, h.count()); |
| ASSERT_DOUBLE_EQ(4.0, h.sum()); |
| } |
| |
| TEST_F(HistogramTest, window_forgets_old_samples) { |
| bvar::Histogram h(bvar::Histogram::BucketSchema({10, 20, 30})); |
| bvar::Window<bvar::Histogram> w(&h, 1); |
| h << 5; |
| sleep(2); |
| // The value recorded 2 seconds ago has left the 1-second window. |
| ASSERT_EQ(0, w.get_value().num); |
| // But the histogram itself still remembers it. |
| ASSERT_EQ(1, h.count()); |
| } |
| |
| struct AddArgs { |
| bvar::Histogram* h; |
| int64_t nvalues; |
| }; |
| |
| static void* add_values(void* arg) { |
| AddArgs* args = (AddArgs*)arg; |
| for (int64_t i = 0; i < args->nvalues; ++i) { |
| // Cycle over the buckets so that every one of them is contended. |
| *args->h << (i % 40); |
| } |
| return nullptr; |
| } |
| |
| TEST_F(HistogramTest, multithreaded) { |
| int64_t nvalues = 20000; |
| bvar::Histogram h(bvar::Histogram::BucketSchema({10, 20, 30})); |
| |
| pthread_t threads[8]; |
| AddArgs args = {&h, nvalues}; |
| for (size_t i = 0; i < arraysize(threads); ++i) { |
| ASSERT_EQ(0, pthread_create(&threads[i], nullptr, add_values, &args)); |
| } |
| for (size_t i = 0; i < arraysize(threads); ++i) { |
| ASSERT_EQ(0, pthread_join(threads[i], nullptr)); |
| } |
| |
| int64_t nrecords = (int64_t)arraysize(threads) * nvalues; |
| bvar::Histogram::Value v = h.get_value(); |
| ASSERT_EQ(nrecords, v.num); |
| // 0..39 repeated, so each bucket gets a quarter of the values. |
| uint64_t per_bucket = (uint64_t)nrecords / 4; |
| ASSERT_EQ(per_bucket + nrecords / 40, v.counts[0]); // 0..10 |
| ASSERT_EQ(v.num, (int64_t)(v.counts[0] + v.counts[1] + |
| v.counts[2] + v.counts[3])); |
| int64_t expected_sum = 0; |
| for (int64_t i = 0; i < 40; ++i) { |
| expected_sum += i; |
| } |
| ASSERT_DOUBLE_EQ((double)expected_sum * nrecords / 40, v.sum); |
| } |
| } // namespace |