Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion source/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ envoy_cc_library(
"@envoy//source/common/http:utility_lib_with_external_headers",
"@envoy//source/common/network:utility_lib_with_external_headers",
"@envoy//source/common/protobuf:utility_lib_with_external_headers",
"@envoy//source/common/stats:stats_lib_with_external_headers",
"@envoy//source/common/stats:histogram_lib_with_external_headers",
"@envoy//source/exe:envoy_common_lib_with_external_headers",
"@envoy//source/server/config_validation:server_lib_with_external_headers",
],
Expand Down
101 changes: 100 additions & 1 deletion source/common/statistic_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,103 @@ nighthawk::client::Statistic HdrStatistic::toProto(SerializationDomain domain) c
return proto;
}

} // namespace Nighthawk
CircllhistStatistic::CircllhistStatistic() {
histogram_ = hist_alloc();
ASSERT(histogram_ != nullptr);
}

CircllhistStatistic::~CircllhistStatistic() { hist_free(histogram_); }

void CircllhistStatistic::addValue(uint64_t value) {
hist_insert_intscale(histogram_, value, 0, 1);
StatisticImpl::addValue(value);
}
double CircllhistStatistic::mean() const { return hist_approx_mean(histogram_); }
double CircllhistStatistic::pvariance() const { return pstdev() * pstdev(); }
double CircllhistStatistic::pstdev() const {
return count() == 0 ? std::nan("") : hist_approx_stddev(histogram_);
}

StatisticPtr CircllhistStatistic::combine(const Statistic& statistic) const {
auto combined = std::make_unique<CircllhistStatistic>();
const auto& stat = dynamic_cast<const CircllhistStatistic&>(statistic);
hist_accumulate(combined->histogram_, &this->histogram_, /*cnt=*/1);
hist_accumulate(combined->histogram_, &stat.histogram_, /*cnt=*/1);

combined->min_ = std::min(this->min(), stat.min());
combined->max_ = std::max(this->max(), stat.max());
combined->count_ = this->count() + stat.count();
return combined;
}

StatisticPtr CircllhistStatistic::createNewInstanceOfSameType() const {
return std::make_unique<CircllhistStatistic>();
}

nighthawk::client::Statistic CircllhistStatistic::toProto(SerializationDomain domain) const {
nighthawk::client::Statistic proto = StatisticImpl::toProto(domain);
if (count() == 0) {
return proto;
}

// List of quantiles is based on hdr_proto_json.gold which lists the quantiles provided by

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I think HdrHistogram will emit more percentiles as the number of (differently-valued) data points added grows. So it's not a fixed set with HdrHistogram.
I am not sure if there is an equivalent way to iterate the available buckets with libcircllhist to get more detail out, and I am also not sure this is really a problem, but I thought it would be good to call out the difference here.

(The only direct implication I figure it would have today is that the Fortio plots would be drawn with less data points)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Otto for the information! Updated the comment to just List of quantiles is based on hdr_proto_json.gold.
From the APIs in circllhist.h, there is no easy way to iterate the quantiles. If the listed data points turn out to be not enough in the future, we can always add more here.

// HdrHistogram.
const std::vector<double> quantiles{0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.55, 0.6,
0.65, 0.7, 0.75, 0.775, 0.8, 0.825, 0.85, 0.875,
0.90, 0.925, 0.95, 0.975, 0.99, 0.995, 0.999, 1};
std::vector<double> computed_quantiles(quantiles.size(), 0.0);
hist_approx_quantile(histogram_, quantiles.data(), quantiles.size(), computed_quantiles.data());
for (size_t i = 0; i < quantiles.size(); i++) {
nighthawk::client::Percentile* percentile = proto.add_percentiles();
if (domain == Statistic::SerializationDomain::DURATION) {
setDurationFromNanos(*percentile->mutable_duration(),
static_cast<int64_t>(computed_quantiles[i]));
} else {
percentile->set_raw_value(computed_quantiles[i]);
}
percentile->set_percentile(quantiles[i]);
percentile->set_count(hist_approx_count_below(histogram_, computed_quantiles[i]));
}

return proto;
}

SinkableStatistic::SinkableStatistic(Envoy::Stats::Scope& scope, absl::optional<int> worker_id)
: Envoy::Stats::HistogramImplHelper(scope.symbolTable()), scope_(scope), worker_id_(worker_id) {
}

SinkableStatistic::~SinkableStatistic() {
// We must explicitly free the StatName here in order to supply the
// SymbolTable reference.
MetricImpl::clear(scope_.symbolTable());
}

Envoy::Stats::Histogram::Unit SinkableStatistic::unit() const {
return Envoy::Stats::Histogram::Unit::Unspecified;
}

Envoy::Stats::SymbolTable& SinkableStatistic::symbolTable() { return scope_.symbolTable(); }

SinkableHdrStatistic::SinkableHdrStatistic(Envoy::Stats::Scope& scope,
absl::optional<int> worker_id)
: SinkableStatistic(scope, worker_id) {}

void SinkableHdrStatistic::recordValue(uint64_t value) {
addValue(value);
// Currently in Envoy Scope implementation, deliverHistogramToSinks() will flush the histogram
// value directly to stats Sinks.
scope_.deliverHistogramToSinks(*this, value);
}

SinkableCircllhistStatistic::SinkableCircllhistStatistic(Envoy::Stats::Scope& scope,
absl::optional<int> worker_id)
: SinkableStatistic(scope, worker_id) {}

void SinkableCircllhistStatistic::recordValue(uint64_t value) {
addValue(value);
// Currently in Envoy Scope implementation, deliverHistogramToSinks() will flush the histogram
// value directly to stats Sinks.
scope_.deliverHistogramToSinks(*this, value);
}

} // namespace Nighthawk
90 changes: 89 additions & 1 deletion source/common/statistic_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "external/dep_hdrhistogram_c/src/hdr_histogram.h"
#include "external/envoy/source/common/common/logger.h"
#include "external/envoy/source/common/stats/histogram_impl.h"

#include "common/frequency.h"

Expand Down Expand Up @@ -150,4 +151,91 @@ class HdrStatistic : public StatisticImpl {
struct hdr_histogram* histogram_;
};

} // namespace Nighthawk
/**
* CircllhistStatistic uses Circllhist under the hood to compute statistics.
* Circllhist is used in the implementation of Envoy Histograms, compared to HdrHistogram it trades
* precision for fast performance in merge and insertion. For more info, please see
* https://github.com/circonus-labs/libcircllhist
*/
class CircllhistStatistic : public StatisticImpl {
public:
CircllhistStatistic();
~CircllhistStatistic() override;

void addValue(uint64_t value) override;
double mean() const override;
double pvariance() const override;
double pstdev() const override;
StatisticPtr combine(const Statistic& statistic) const override;
uint64_t significantDigits() const override { return 1; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a low guarantee; I wonder, can we improve? (HdrStatistic manages 4 significant digits in the way it we set it up).

@qqustc qqustc Jul 7, 2020

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. Confirmed that circllhist only has 2 digit decimal precision as a result of base 10 algorithm.
#115 (comment)

Since this is out of current work scope, decided to leave the discussion of potential impacts for future work when we try to switch to circllhist

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we move the implementation into the .cc file, we should add a comment explaining the single digit precision.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment may have been missed. Even if we end up not moving this to the .cc file, we should add a comment explaining the choice of "1" as the precision.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you mum4k@.
Added a comment here.

StatisticPtr createNewInstanceOfSameType() const override;
nighthawk::client::Statistic toProto(SerializationDomain domain) const override;

private:
histogram_t* histogram_;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(optional) Using a raw pointer here opens us to memory leaks and all the usual problems of raw pointers.

Would it be possible to move this to a smart pointer (std::unique_ptr) with a custom deleter?

https://www.bfilipek.com/2016/04/custom-deleters-for-c-smart-pointers.html

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Jakub for the reference. It seems the hist_free(histogram_t *hist) is quite complicated, so I will not move it to a smart pointer for now

};

/**
* In order to be able to flush a histogram value to downstream Envoy stats Sinks, abstract class
* SinkableStatistic takes the Scope reference in the constructor and wraps the
* Envoy::Stats::HistogramHelper interface. Implementation of sinkable Nighthawk Statistic class
* will inherit from this class.
*/
class SinkableStatistic : public Envoy::Stats::HistogramImplHelper {
public:
// Calling HistogramImplHelper(SymbolTable& symbol_table) constructor to construct an empty
// MetricImpl. This is to bypass the complicated logic of setting up SymbolTable/StatName in
// Envoy.
SinkableStatistic(Envoy::Stats::Scope& scope, absl::optional<int> worker_id);
~SinkableStatistic() override;

// Currently Envoy Histogram Unit supports {Unspecified, Bytes, Microseconds, Milliseconds}. By
// default, Nighthawk::Statistic uses nanosecond as the unit of latency histograms, so Unspecified
// is returned here to isolate Nighthawk Statistic from Envoy Histogram Unit.
Envoy::Stats::Histogram::Unit unit() const override;
Envoy::Stats::SymbolTable& symbolTable() override;
// Return the id of the worker where this statistic is defined. Per worker
// statistic should always set worker_id. Return absl::nullopt when the
// statistic is not defined per worker.
const absl::optional<int> worker_id() { return worker_id_; }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this one isn't an override and is public, we should document it. We should also explicitly say why does it return absl::optional and what does it mean when the value isn't present.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. PTAL


// This is used for delivering the histogram data to sinks.
Envoy::Stats::Scope& scope_;

private:
// worker_id can be used in downstream stats Sinks as the stats tag.
absl::optional<int> worker_id_;
Comment thread
oschaaf marked this conversation as resolved.
Outdated
};

// Implementation of sinkable Nighthawk Statistic with HdrHistogram.
class SinkableHdrStatistic : public SinkableStatistic, public HdrStatistic {
public:
// The constructor takes the Scope reference which is used to flush a histogram value to
// downstream stats Sinks through deliverHistogramToSinks().
SinkableHdrStatistic(Envoy::Stats::Scope& scope, absl::optional<int> worker_id = absl::nullopt);

// Envoy::Stats::Histogram
void recordValue(uint64_t value) override;
bool used() const override { return count() > 0; }
// Overriding name() to return Nighthawk::Statistic::id().
std::string name() const override { return id(); }
std::string tagExtractedName() const override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; }
};

// Implementation of sinkable Nighthawk Statistic with Circllhist Histogram.
class SinkableCircllhistStatistic : public SinkableStatistic, public CircllhistStatistic {
public:
// The constructor takes the Scope reference which is used to flush a histogram value to
// downstream stats Sinks through deliverHistogramToSinks().
SinkableCircllhistStatistic(Envoy::Stats::Scope& scope,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document the constructor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

absl::optional<int> worker_id = absl::nullopt);

// Envoy::Stats::Histogram
void recordValue(uint64_t value) override;
bool used() const override { return count() > 0; }
// Overriding name() to return Nighthawk::Statistic::id().
std::string name() const override { return id(); }
std::string tagExtractedName() const override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; }
};

} // namespace Nighthawk
6 changes: 5 additions & 1 deletion test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,17 @@ envoy_cc_test(
envoy_cc_test(
name = "statistic_test",
srcs = ["statistic_test.cc"],
data = ["test_data/hdr_proto_json.gold"],
data = [
"test_data/circllhist_proto_json.gold",
"test_data/hdr_proto_json.gold",
],
repository = "@envoy",
deps = [
"//source/common:nighthawk_common_lib",
"//test/test_common:environment_lib",
"@envoy//source/common/protobuf:utility_lib_with_external_headers",
"@envoy//source/common/stats:isolated_store_lib_with_external_headers",
"@envoy//test/mocks/stats:stats_mocks",
],
)

Expand Down
4 changes: 2 additions & 2 deletions test/run_nighthawk_bazel_coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ COVERAGE_VALUE=${COVERAGE_VALUE%?}

if [ "$VALIDATE_COVERAGE" == "true" ]
then
COVERAGE_THRESHOLD=98.6
COVERAGE_THRESHOLD=98.4

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we forced to lower the threshold? Which lines / behaviors we can't cover in this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://14282-180498819-gh.circle-artifacts.com/0/coverage/source/common/statistic_impl.h.gcov.html

The lines not covered are std::string tagExtractedName() const override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; }, where I added test EXPECT_DEATH(stat.tagExtractedName(), ".*"); but it does not count into coverage. So after talked to Otto, we decided to lower the coverage.

COVERAGE_FAILED=$(echo "${COVERAGE_VALUE}<${COVERAGE_THRESHOLD}" | bc)
if test ${COVERAGE_FAILED} -eq 1; then
echo Code coverage ${COVERAGE_VALUE} is lower than limit of ${COVERAGE_THRESHOLD}
Expand All @@ -52,4 +52,4 @@ then
echo Code coverage ${COVERAGE_VALUE} is good and higher than limit of ${COVERAGE_THRESHOLD}
fi
fi
echo "HTML coverage report is in ${COVERAGE_DIR}/index.html"
echo "HTML coverage report is in ${COVERAGE_DIR}/index.html"
Loading