blob: b9c87a824a9e91d1bc5f3cd9c9414cc0f632da19 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "client/collection/observations_collector.h"
#include "gflags/gflags.h"
#include "glog/logging.h"
#include "third_party/googletest/googletest/include/gtest/gtest.h"
const int64_t kPeriodSize = 1000;
const int64_t kPeriodCount = 1000;
const int64_t kThreadNum = 100;
const int64_t kSamplerMaxInt = 20;
namespace cobalt {
namespace client {
namespace {
// Function that increments a counter kPeriodSize * kPeriodCount times in
// kPeriodCount increments with some random jitter in between.
void DoIncrement(std::shared_ptr<Counter> counter) {
for (int64_t i = 0; i < kPeriodCount; i++) {
for (int64_t j = 0; j < kPeriodSize; j++) {
counter->Increment();
}
// Introduce jitter to test.
std::this_thread::sleep_for(std::chrono::microseconds(std::rand() % 100));
}
}
// Sink is used to gather all the observations sent by the MetricFactory.
struct Sink {
explicit Sink(bool with_errors) : with_errors(with_errors) {}
std::vector<size_t> SendObservations(std::vector<Observation>* obs) {
std::vector<size_t> errors;
for (auto iter = std::make_move_iterator(obs->begin());
std::make_move_iterator(obs->end()) != iter; iter++) {
// Randomly fail to "send" some observations.
if (with_errors && std::rand() % 5 == 0) {
errors.push_back(
std::distance(std::make_move_iterator(obs->begin()), iter));
continue;
}
observations.push_back(*iter);
}
return errors;
}
std::vector<Observation> observations;
bool with_errors;
};
} // namespace
// Checks that Counters work correctly with many threads updating them.
TEST(Counter, Normal) {
// Metric id.
const int64_t kMetricId = 10;
Sink sink(true);
ObservationsCollector collector(
std::bind(&Sink::SendObservations, &sink, std::placeholders::_1), 1);
auto counter = collector.MakeCounter(kMetricId, "part_name");
// Each thread will add kPeriodSize * kPeriodCount to the counter.
int64_t expected = kPeriodSize * kPeriodCount * kThreadNum;
std::vector<std::thread> threads;
// Start all the incrementer threads.
for (int i = 0; i < kThreadNum; i++) {
threads.push_back(std::thread(DoIncrement, counter));
}
// Start the collection thread.
collector.Start(std::chrono::microseconds(10));
// Wait until all the incrementer threads have finished.
for (auto iter = threads.begin(); iter != threads.end(); iter++) {
iter->join();
}
// Stop the collection thread.
collector.Stop();
// Add up all the observations in the sink.
int64_t actual = 0;
for (auto iter = sink.observations.begin(); sink.observations.end() != iter;
iter++) {
actual += (*iter).parts[0].value.GetIntValue();
}
EXPECT_EQ(expected, actual);
}
// Function that logs kPeriodSize * kPeriodCount random integers to an
// IntegerSampler in kPeriodCount increments with some random jitter in between.
void DoLogObservation(std::shared_ptr<IntegerSampler> int_sampler) {
std::random_device rd;
std::default_random_engine gen(rd());
// Uniformly sample from the set [0...kSamplerMaxInt].
std::uniform_int_distribution<> dis(0, kSamplerMaxInt);
for (int64_t i = 0; i < kPeriodCount; i++) {
for (int64_t j = 0; j < kPeriodSize; j++) {
int_sampler->LogObservation(dis(gen));
}
// Introduce jitter to test.
std::this_thread::sleep_for(std::chrono::microseconds(std::rand() % 100));
}
}
// Test that the average of the samples is within an expected range of the
// theoretical average.
TEST(IntegerSampler, IntegerAverage) {
// Metric id.
const int64_t kMetricId = 10;
Sink sink(false);
ObservationsCollector collector(
std::bind(&Sink::SendObservations, &sink, std::placeholders::_1), 1);
// The IntegerSampler will collect at most 100 elements at a time.
size_t sample_size = 100;
auto int_sampler =
collector.MakeIntegerSampler(kMetricId, "part_name", sample_size);
// Each thread will log kPeriodSize * kPeriodCount times to the
// IntegerSampler.
std::vector<std::thread> threads;
for (int i = 0; i < kThreadNum; i++) {
threads.push_back(std::thread(DoLogObservation, int_sampler));
}
// Start the collection thread.
collector.Start(std::chrono::microseconds(10));
// Wait until all the logging threads have finished.
for (auto iter = threads.begin(); iter != threads.end(); iter++) {
iter->join();
}
// Stop the collection thread.
collector.Stop();
// In the worst case scenario, the number of collected observations is
// kPeriodSize * kPeriodCount * kThreadNum.
// In the worst case scenario, where every generated number is 20, the total
// is 2*10^9. This comfortably fits in a 64 bits signed integer.
int64_t total = 0;
int64_t num_obs = 0;
for (auto iter = sink.observations.begin(); sink.observations.end() != iter;
iter++) {
num_obs++;
total += (*iter).parts[0].value.GetIntValue();
}
double sample_mean =
static_cast<double>(total) / static_cast<double>(num_obs);
// We sample num_obs elements from the uniform distribution
// [0...kSamplerMaxInt]. The central limit theorem tells us the average of
// these num_obs samples will be normally distributed with the same mean as
// the uniform distribution and a variance equal to 1/num_obs times the
// uniform distribution's variance.
double expected_mean = kSamplerMaxInt / 2;
double expected_stddev =
std::sqrt((kSamplerMaxInt + 1) * (kSamplerMaxInt) / 12.0 / num_obs);
// We test to see if the sample mean is within 4.5 standard deviations of
// the expected mean. This should prevent false positives at least 99.999% of
// the time.
EXPECT_NEAR(expected_mean, sample_mean, expected_stddev * 4.5);
}
// Test that if a source of data has a very strong time-dependent bias, the
// IntegerSampler does not reflect that time-dependency.
TEST(IntegerSampler, CheckUniformity) {
// Metric id.
const int64_t kMetricId = 10;
Sink sink(false);
ObservationsCollector collector(
std::bind(&Sink::SendObservations, &sink, std::placeholders::_1), 1);
// The IntegerSampler will collect at most 100 elements at a time.
size_t sample_size = 100;
auto int_sampler =
collector.MakeIntegerSampler(kMetricId, "part_name", sample_size);
// We log integers in increasing order. This is to check that the ordering of
// the logged observations is not related to the distribution of the sampled
// observations.
for (int64_t val = 0; val <= kSamplerMaxInt; val++) {
for (size_t i = 0; i < sample_size * 10; i++) {
int_sampler->LogObservation(val);
}
}
collector.CollectAll();
int64_t total = 0;
int64_t num_obs = 0;
for (auto iter = sink.observations.begin(); sink.observations.end() != iter;
iter++) {
num_obs++;
total += (*iter).parts[0].value.GetIntValue();
}
double sample_mean =
static_cast<double>(total) / static_cast<double>(num_obs);
// We sample num_obs elements from the uniform distribution
// [0...kSamplerMaxInt]. The central limit theorem tells us the average of
// these num_obs samples will be normally distributed with the same mean as
// the uniform distribution and a variance equal to 1/num_obs times the
// uniform distribution's variance.
double expected_mean = kSamplerMaxInt / 2;
double expected_stddev =
std::sqrt((kSamplerMaxInt + 1) * (kSamplerMaxInt) / 12.0 / num_obs);
// We test to see if the sample mean is within 4.5 standard deviations of
// the expected mean. This should prevent false positives at least 99.999% of
// the time.
EXPECT_NEAR(expected_mean, sample_mean, expected_stddev * 4.5);
}
void DoLogEvent(std::shared_ptr<EventLogger> logger, uint32_t status) {
std::random_device rd;
std::default_random_engine gen(rd());
// Uniformly sample from the set [0...kSamplerMaxInt].
std::uniform_int_distribution<> time(0, kSamplerMaxInt);
for (int64_t i = 0; i < kPeriodCount; i++) {
for (int64_t j = 0; j < kPeriodSize; j++) {
logger->LogEvent(time(gen), status);
}
// Introduce jitter to test.
std::this_thread::sleep_for(std::chrono::microseconds(std::rand() % 100));
}
}
TEST(EventLogger, Normal) {
const uint32_t kEventMetricId = 1;
const uint32_t kMaxStatus = 4;
const uint32_t kEventTimingMetricId = 2;
const size_t kThreadNum = 100;
ASSERT_EQ(kThreadNum % (kMaxStatus + 1), size_t(0))
<< "kThreadNum must be divisible by the number of statuses.";
size_t samples = 100;
Sink sink(false);
ObservationsCollector collector(
std::bind(&Sink::SendObservations, &sink, std::placeholders::_1), 1);
auto logger = collector.MakeEventLogger(kEventMetricId, kMaxStatus,
kEventTimingMetricId, samples);
std::vector<std::thread> threads;
for (size_t i = 0; i < kThreadNum; i++) {
uint32_t status = i % (kMaxStatus + 1);
threads.push_back(std::thread(DoLogEvent, logger, status));
}
// Start the collection thread.
collector.Start(std::chrono::microseconds(10));
// Wait until all the logging threads have finished.
for (auto iter = threads.begin(); iter != threads.end(); iter++) {
iter->join();
}
// Stop the collection thread.
collector.Stop();
// We sum up the distributions and check the sum is as expected.
int64_t histogram[kMaxStatus + 1] = {0, 0, 0, 0, 0};
for (auto iter = sink.observations.begin(); sink.observations.end() != iter;
iter++) {
if (iter->metric_id != kEventMetricId) continue;
for (auto part_iter = iter->parts.begin(); iter->parts.end() != part_iter;
part_iter++) {
if (part_iter->part_name == "status") {
auto dist = part_iter->value.GetDistribution();
for (auto status_iter = dist.begin(); dist.end() != status_iter;
status_iter++) {
histogram[status_iter->first] += status_iter->second;
}
}
}
}
int64_t expected_histogram_value =
kThreadNum / (kMaxStatus + 1) * kPeriodSize * kPeriodCount;
for (size_t status = 0; status <= kMaxStatus; status++) {
EXPECT_EQ(histogram[status], expected_histogram_value);
}
}
// Check that the integer value part work correctly.
TEST(ValuePart, IntValuePart) {
ValuePart value = ValuePart::MakeInt(10);
EXPECT_EQ(10, value.GetIntValue());
EXPECT_TRUE(value.IsIntValue());
EXPECT_EQ(ValuePart::INT, value.Which());
}
TEST(ValuePart, DoubleValuePart) {
ValuePart value = ValuePart::MakeDouble(10.5);
EXPECT_DOUBLE_EQ(10.5, value.GetDoubleValue());
EXPECT_TRUE(value.IsDoubleValue());
EXPECT_EQ(ValuePart::DOUBLE, value.Which());
}
TEST(ValuePart, DistributionValuePart) {
std::map<uint32_t, int64_t> distribution = {{0, 1}, {1, 2}, {2, 4}};
ValuePart value = ValuePart::MakeDistribution(distribution);
EXPECT_EQ(distribution.size(), value.GetDistribution().size());
EXPECT_TRUE(value.IsDistribution());
EXPECT_EQ(ValuePart::DISTRIBUTION, value.Which());
// Check that the copy constructor works.
ValuePart copy = value;
EXPECT_EQ(distribution.size(), copy.GetDistribution().size());
EXPECT_TRUE(copy.IsDistribution());
EXPECT_EQ(ValuePart::DISTRIBUTION, copy.Which());
}
} // namespace client
} // namespace cobalt