Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
100 changes: 99 additions & 1 deletion source/common/statistic_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,102 @@ 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.
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
93 changes: 92 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,94 @@ 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;
// circllhist has low significant digit precision as a result of base 10
// algorithm.
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).

Copy link
Copy Markdown
Contributor Author

@qqustc qqustc Jul 7, 2020

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


protected:
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 you help me understand why this is protected rather than public?

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.

Ah, there is no special reason other than to make its access more restrictive. So changed it to public.

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.

I apologize, I made a typo in the comment. I meant to say "rather than private". We should always prefer private methods and fields as they minimize the public API and allow us to execute faster (internal changes).

Can this be private instead?

// This is used in child class 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_;
};

// 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