Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HeapStatData with a distinct allocation mechanism for RawStatData #3710

Merged
merged 35 commits into from
Jul 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a7f5585
templatize RawStatDataAllocator -- not functional yet.
jmarantz Jun 22, 2018
d5e88bf
Sink CounterImpl and GaugeImpl into stats_impl.cc; no need to have th…
jmarantz Jun 22, 2018
be3cfee
Merge branch 'refactor-admin-stats' into stats-heap-divest
jmarantz Jun 22, 2018
dc1f5a5
more templatization hacks.
jmarantz Jun 22, 2018
27f1d9e
Merge branch 'master' into stats-heap-divest
jmarantz Jun 22, 2018
ee8cf0b
Introduce a pure heap-based variant of RawStatsData.
jmarantz Jun 23, 2018
29741c2
Merge branch 'master' into refactor-admin-stats
jmarantz Jun 23, 2018
3513338
Merge branch 'refactor-admin-stats' into stats-heap-divest
jmarantz Jun 23, 2018
7204822
Merge branch 'master' into stats-heap-divest
jmarantz Jun 23, 2018
6c09c7c
experimental commit that merges the two variants of Heap*StatDataAllo…
jmarantz Jun 25, 2018
d427401
remove superfluous blank linke.
jmarantz Jun 25, 2018
e21dd1d
Merge branch 'master' into stats-heap-divest
jmarantz Jul 18, 2018
343eae5
Checkpointing all tests build (2 fail)
jmarantz Jul 19, 2018
d809c61
all tests pass.
jmarantz Jul 19, 2018
5d3e916
Formatting and other cleanup.
jmarantz Jul 19, 2018
c79f73d
Test that truncation actually occurred in Consistency test.
jmarantz Jul 19, 2018
5c6c925
Add 'const' declarations for locals.
jmarantz Jul 19, 2018
e9d78cf
Use an effectively unlimited truncation size (1M) for IsolatedStore f…
jmarantz Jul 20, 2018
77eecc5
Remove some redundant code and cleanup.
jmarantz Jul 23, 2018
8ba7c45
Use std::numeric_limits for unbounded length, dividing by 2 to avoid …
jmarantz Jul 26, 2018
85a91de
Document required fields for StatData.
jmarantz Jul 26, 2018
83a3797
Merge branch 'master' into stats-heap-divest
jmarantz Jul 27, 2018
42aed4a
Re-order functions to reduce diffs.
jmarantz Jul 27, 2018
f48bf77
add a comment justifying use of templates for the StatAllocator.
jmarantz Jul 27, 2018
ccb29e1
Fix typos
jmarantz Jul 27, 2018
9a757c2
Add more commentary about why templates are used rather than virtual …
jmarantz Jul 27, 2018
7f84547
Move all truncation to thread_local_store.cc.
jmarantz Jul 28, 2018
7d15bf8
Move truncation test to thread_local_store_test.cc, and simplify code.
jmarantz Jul 29, 2018
5f82615
Cleanup and restore truncation-message log test.
jmarantz Jul 29, 2018
8dbaff2
Return nullptr when HotRestartImpl::alloc is asked for a name too lar…
jmarantz Jul 29, 2018
700d540
style fixes & typos
jmarantz Jul 30, 2018
319462b
Address review comments, style issues, etc.
jmarantz Jul 30, 2018
c0f9499
Use ASSERT for length-check when allocating stats in hot-restart.
jmarantz Jul 30, 2018
1b89d7f
Address review nits.
jmarantz Jul 31, 2018
08fe789
Correct spelling of StatDataAllocator -- this time it will stick for …
jmarantz Jul 31, 2018
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
9 changes: 7 additions & 2 deletions include/envoy/stats/stats.h
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ class StatDataAllocator {
* @return CounterSharedPtr a counter, or nullptr if allocation failed, in which case
* tag_extracted_name and tags are not moved.
*/
virtual CounterSharedPtr makeCounter(const std::string& name, std::string&& tag_extracted_name,
virtual CounterSharedPtr makeCounter(absl::string_view name, std::string&& tag_extracted_name,
std::vector<Tag>&& tags) PURE;

/**
Expand All @@ -477,9 +477,14 @@ class StatDataAllocator {
* @return GaugeSharedPtr a gauge, or nullptr if allocation failed, in which case
* tag_extracted_name and tags are not moved.
*/
virtual GaugeSharedPtr makeGauge(const std::string& name, std::string&& tag_extracted_name,
virtual GaugeSharedPtr makeGauge(absl::string_view name, std::string&& tag_extracted_name,
std::vector<Tag>&& tags) PURE;

/**
* Determines whether this stats allocator requires bounded stat-name size.
*/
virtual bool requiresBoundedStatNameSize() const PURE;

// TODO(jmarantz): create a parallel mechanism to instantiate histograms. At
// the moment, histograms don't fit the same pattern of counters and gaugaes
// as they are not actually created in the context of a stats allocator.
Expand Down
2 changes: 1 addition & 1 deletion source/common/common/block_memory_hash_set.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ template <class Value> class BlockMemoryHashSet : public Logger::Loggable<Logger
cell.next_cell_index = slots_[slot];
slots_[slot] = cell_index;
value = &cell.value;
value->truncateAndInit(key, stats_options_);
value->initialize(key, stats_options_);
++control_->size;
return ValueCreatedPair(value, true);
}
Expand Down
119 changes: 53 additions & 66 deletions source/common/stats/stats_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -135,35 +135,35 @@ bool TagExtractorImpl::extractTag(const std::string& stat_name, std::vector<Tag>
return false;
}

RawStatData* HeapRawStatDataAllocator::alloc(const std::string& name) {
uint64_t num_bytes_to_allocate = RawStatData::structSize(name.size());
RawStatData* data = static_cast<RawStatData*>(::calloc(num_bytes_to_allocate, 1));
if (data == nullptr) {
throw std::bad_alloc();
}
data->checkAndInit(name, num_bytes_to_allocate);
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're getting rid of checkAndInit, is there other cleanup in the regular RawStatData struct we can do now, now that it's been decoupled from HeapStatData? Maybe RawStatData, if only for blocked / shared memory, can be less general and more opinionated about its internal memory representation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we're getting rid of checkAndInit. It is still needed in the shm case. I don't think anything gets easier by knowing it's always shm.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

correction...checkAndInit is just merged with initialize() now.

HeapStatData::HeapStatData(absl::string_view key) : name_(key.data(), key.size()) {}

HeapStatData* HeapStatDataAllocator::alloc(absl::string_view name) {
// Any expected truncation of name is done at the callsite. No truncation is
// required to use this allocator.
auto data = std::make_unique<HeapStatData>(name);
Thread::ReleasableLockGuard lock(mutex_);
auto ret = stats_.insert(data);
RawStatData* existing_data = *ret.first;
auto ret = stats_.insert(data.get());
HeapStatData* existing_data = *ret.first;
lock.release();

if (!ret.second) {
::free(data);
++existing_data->ref_count_;
return existing_data;
} else {
return data;
if (ret.second) {
return data.release();
}
++existing_data->ref_count_;
return existing_data;
}

/**
* Counter implementation that wraps a RawStatData.
* Counter implementation that wraps a StatData. StatData must have data members:
* std::atomic<int64_t> value_;
* std::atomic<int64_t> pending_increment_;
* std::atomic<int16_t> flags_;
* std::atomic<int16_t> ref_count_;
*/
class CounterImpl : public Counter, public MetricImpl {
template <class StatData> class CounterImpl : public Counter, public MetricImpl {
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to use templates here vs. polymorphism? Would prefer the latter if possible for the usual template reasons (horrible compiler errors being my pet peeve).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not following your suggestion. AFAIK polymorphism is not an alternative to templates, for code-sharing the class bodies. The most direct alternative is macros, and a functional (but less performance and memory-efficient) substitute in this case would be virtual inheritance from a base class. I think templates are the correct solution here.

Copy link
Member

Choose a reason for hiding this comment

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

Polymorphism == virtual inheritance for the purpose of my comment :) OK, I'm willing to buy templates for performance reasons since stats increment etc. is actually on the hot path. I think you should add justification at the template declaration sites explicitly calling this out, since we default to "avoid templates unless absolutely necessary" in Envoy, an maybe someone comes along and undoes this if you don't ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Member

Choose a reason for hiding this comment

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

Just to toss my hat in here - as I mentioned above, I agree with Harvey that the Envoy default is to avoid templates as much as possible. I agree that this is the most performant solution for sharing code that I can think of. However, it seems like the majority of the Envoy codebase (even portions in the hot path) use virtual function calls everywhere. This feels a little bit like premature optimization. We already do one virtual function call into Counter and Gauge per change since all the functions on those classes are virtual. Do we have an idea how much an extra virtual call here will affect real-world performance?

I'm not super opinionated on this, and if no one else has issues with using templates this extensively, we can leave it as is, but it seems like this does go against general Envoy convention.

Copy link
Member

Choose a reason for hiding this comment

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

It's about 5 instructions for a virtual dispatch. I think it's very likely premature optimization but I didn't look into the change in very deep detail.

Copy link
Contributor Author

@jmarantz jmarantz Jul 27, 2018

Choose a reason for hiding this comment

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

Ah -- you are right. this all goes through CounterImpl::inc() which is a virtual override, so it isn't hyper-optimized anyway, but two virtual function calls are still more expensive than one.

I still feel like templates are a natural expression of this code though; I've been doing templates for so long that I don't have any kind of qualms about them.

But actually -- I wrote this code like 2 months ago and it's coming back to me now -- I think the real reason I wanted to stay away from virtual inheritance her is that the vptr may not work well in shared memory since it's a pointer. I will add that to the comment.

Also, I did a quick recursive grep:

rgrepn "template <class" "*.h"|wc -l

and found there are 93 template class in the Envoy codebase, including the ones I've added here. So it may not be the most common approach but it's certainly not rare.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added comments in code:

//
// Also note that RawStatData needs to live in a shared memory block, and it's
// possible, but not obvious, that a vptr would be usable across processes. In
// any case, RawStatData is currently allocated with calloc() rather than new,
// so the usual C++ compiler assistance for setting up vptrs will not be
// available. This could be resolved with placed new.

Copy link
Member

Choose a reason for hiding this comment

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

I think the way you would implement this with virtual inheritance is by wrapping a reference to the struct that is stored in shared memory in a virtual class that would be local to a single instance of Envoy. This is similar to how CounterImpl, etc works today - just more nested.

Anyway, I'll yield to @mattklein123 and @htuch on this since they are much more aware of what the overall Envoy conventions and perf tradeoffs are.

Copy link
Member

Choose a reason for hiding this comment

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

If the code is cleaner w/ templates it's fine. I think the high level guidance is to think about whether templates make the code more readable or not. In many cases the answer is not but this seems fine if you think it's better.

public:
CounterImpl(RawStatData& data, RawStatDataAllocator& alloc, std::string&& tag_extracted_name,
std::vector<Tag>&& tags)
CounterImpl(StatData& data, StatDataAllocatorImpl<StatData>& alloc,
std::string&& tag_extracted_name, std::vector<Tag>&& tags)
: MetricImpl(data.name_, std::move(tag_extracted_name), std::move(tags)), data_(data),
alloc_(alloc) {}
~CounterImpl() { alloc_.free(data_); }
Expand All @@ -182,17 +182,17 @@ class CounterImpl : public Counter, public MetricImpl {
uint64_t value() const override { return data_.value_; }

private:
RawStatData& data_;
RawStatDataAllocator& alloc_;
StatData& data_;
StatDataAllocatorImpl<StatData>& alloc_;
};

/**
* Gauge implementation that wraps a RawStatData.
* Gauge implementation that wraps a StatData.
*/
class GaugeImpl : public Gauge, public MetricImpl {
template <class StatData> class GaugeImpl : public Gauge, public MetricImpl {
public:
GaugeImpl(RawStatData& data, RawStatDataAllocator& alloc, std::string&& tag_extracted_name,
std::vector<Tag>&& tags)
GaugeImpl(StatData& data, StatDataAllocatorImpl<StatData>& alloc,
std::string&& tag_extracted_name, std::vector<Tag>&& tags)
: MetricImpl(data.name_, std::move(tag_extracted_name), std::move(tags)), data_(data),
alloc_(alloc) {}
~GaugeImpl() { alloc_.free(data_); }
Expand All @@ -217,8 +217,8 @@ class GaugeImpl : public Gauge, public MetricImpl {
bool used() const override { return data_.flags_ & Flags::Used; }

private:
RawStatData& data_;
RawStatDataAllocator& alloc_;
StatData& data_;
StatDataAllocatorImpl<StatData>& alloc_;
};

TagProducerImpl::TagProducerImpl(const envoy::config::metrics::v2::StatsConfig& config) {
Expand Down Expand Up @@ -319,47 +319,28 @@ TagProducerImpl::addDefaultExtractors(const envoy::config::metrics::v2::StatsCon
return names;
}

void HeapRawStatDataAllocator::free(RawStatData& data) {
// TODO(jmarantz): move this below HeapStatDataAllocator::alloc.
Copy link
Member

Choose a reason for hiding this comment

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

? Seems like a strange TODO?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Normally it makes sense to group alloc/free together of a single class, but these got separated in a prior PR; leaving this here now for easier diffs. Will fix later in a follow-up.

Copy link
Member

Choose a reason for hiding this comment

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

I see. It seemed so trivial, I was a little confused as to why you didn't just do it now. Easier review is a good reason. That makes sense.

void HeapStatDataAllocator::free(HeapStatData& data) {
ASSERT(data.ref_count_ > 0);
if (--data.ref_count_ > 0) {
return;
}

size_t key_removed;
{
Thread::LockGuard lock(mutex_);
key_removed = stats_.erase(&data);
size_t key_removed = stats_.erase(&data);
ASSERT(key_removed == 1);
}

ASSERT(key_removed == 1);
::free(&data);
delete &data;
}

void RawStatData::initialize(absl::string_view key, uint64_t xfer_size) {
void RawStatData::initialize(absl::string_view key, const StatsOptions& stats_options) {
ASSERT(!initialized());
ASSERT(key.size() <= stats_options.maxNameLength());
ref_count_ = 1;
memcpy(name_, key.data(), xfer_size);
name_[xfer_size] = '\0';
}

void RawStatData::checkAndInit(absl::string_view key, uint64_t num_bytes_allocated) {
uint64_t xfer_size = key.size();
ASSERT(structSize(xfer_size) <= num_bytes_allocated);

initialize(key, xfer_size);
}

void RawStatData::truncateAndInit(absl::string_view key, const StatsOptions& stats_options) {
if (key.size() > stats_options.maxNameLength()) {
ENVOY_LOG_MISC(
warn,
"Statistic '{}' is too long with {} characters, it will be truncated to {} characters", key,
key.size(), stats_options.maxNameLength());
}

// key is not necessarily nul-terminated, but we want to make sure name_ is.
uint64_t xfer_size = std::min(stats_options.maxNameLength(), key.size());
initialize(key, xfer_size);
memcpy(name_, key.data(), key.size());
name_[key.size()] = '\0';
}

HistogramStatisticsImpl::HistogramStatisticsImpl(const histogram_t* histogram_ptr)
Expand Down Expand Up @@ -420,26 +401,32 @@ void SourceImpl::clearCache() {
histograms_.reset();
}

CounterSharedPtr RawStatDataAllocator::makeCounter(const std::string& name,
std::string&& tag_extracted_name,
std::vector<Tag>&& tags) {
RawStatData* data = alloc(name);
template <class StatData>
CounterSharedPtr StatDataAllocatorImpl<StatData>::makeCounter(absl::string_view name,
std::string&& tag_extracted_name,
std::vector<Tag>&& tags) {
StatData* data = alloc(name);
if (data == nullptr) {
return nullptr;
}
return std::make_shared<CounterImpl>(*data, *this, std::move(tag_extracted_name),
std::move(tags));
return std::make_shared<CounterImpl<StatData>>(*data, *this, std::move(tag_extracted_name),
std::move(tags));
}

GaugeSharedPtr RawStatDataAllocator::makeGauge(const std::string& name,
std::string&& tag_extracted_name,
std::vector<Tag>&& tags) {
RawStatData* data = alloc(name);
template <class StatData>
GaugeSharedPtr StatDataAllocatorImpl<StatData>::makeGauge(absl::string_view name,
std::string&& tag_extracted_name,
std::vector<Tag>&& tags) {
StatData* data = alloc(name);
if (data == nullptr) {
return nullptr;
}
return std::make_shared<GaugeImpl>(*data, *this, std::move(tag_extracted_name), std::move(tags));
return std::make_shared<GaugeImpl<StatData>>(*data, *this, std::move(tag_extracted_name),
std::move(tags));
}

template class StatDataAllocatorImpl<HeapStatData>;
template class StatDataAllocatorImpl<RawStatData>;

} // namespace Stats
} // namespace Envoy
Loading