Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3d5849d
First working draft of simpler reference-counted heap allocator
ambuc Apr 25, 2018
b6aa029
Fixed reference to pointer to RawStatData
ambuc Apr 25, 2018
d615eef
Add HeapAlloc test for HeapRawStatDataAllocator
ambuc Apr 25, 2018
cdd8418
Initialized data based on truncated name
ambuc Apr 25, 2018
101c7aa
Remove leftover comments
ambuc Apr 25, 2018
282c56b
Initialized data based on non-truncated name
ambuc Apr 25, 2018
830c718
Initialized data based on truncated name, and also warn in both alloc…
ambuc Apr 25, 2018
4de411a
Remove leftover couts
ambuc Apr 25, 2018
11a8cdb
First working draft of simpler reference-counted heap allocator
ambuc Apr 25, 2018
ecd7714
Fixed reference to pointer to RawStatData
ambuc Apr 25, 2018
8c00102
Add HeapAlloc test for HeapRawStatDataAllocator
ambuc Apr 25, 2018
ddcb0f9
Initialized data based on truncated name
ambuc Apr 25, 2018
7031caa
Remove leftover comments
ambuc Apr 25, 2018
93eb3d7
Initialized data based on non-truncated name
ambuc Apr 25, 2018
b4b1b39
Initialized data based on truncated name, and also warn in both alloc…
ambuc Apr 25, 2018
7e2b8d6
Remove leftover couts
ambuc Apr 25, 2018
96582ad
Merge branch 'refcount-stats-in-heap-alloc' of https://github.com/amb…
ambuc Apr 27, 2018
bec4fac
Fix free stat issue, typedef datamap, assorted
ambuc Apr 27, 2018
ea27f46
Add mutex to HeapRawStatDataAllocator
ambuc Apr 30, 2018
a4eb61e
Rename mutex, make non-mutable
ambuc Apr 30, 2018
b388492
Use unordered_set<> for HeapRawStatDataAllocator's stats
ambuc May 1, 2018
3b9d839
Tighten mutex; remove temp key
ambuc May 1, 2018
b10f663
Minimize time spent holding lock
ambuc May 1, 2018
759e6d9
Fix camelCase style issue
ambuc May 1, 2018
8a8a39d
Add more robust null checking around RawDataTest/Truncate
ambuc May 1, 2018
f0e19ae
Change EXPECTs to ASSERTs in RawStatDataTest/Truncate
ambuc May 1, 2018
76ebd08
Tighten mutex around HeapRawStatDataAllocator::free()
ambuc May 1, 2018
87318c0
More specific asserts around unordered_map::erase behavior
ambuc May 2, 2018
84913f9
Merge branch 'master' into refcount-stats-in-heap-alloc
ambuc May 2, 2018
5194ec2
Add GUARDED_BY to mutex_, documentation around StringRawDataSet
ambuc May 3, 2018
ce99186
Merge branch 'master' into refcount-stats-in-heap-alloc
ambuc May 3, 2018
5429301
Formatting updates
ambuc May 3, 2018
40ae83b
Add documentation for HeapRawStatDataAllocator
ambuc May 4, 2018
20526e7
Edits for documentation for HeapRawStatDataAllocator
ambuc May 4, 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
31 changes: 25 additions & 6 deletions source/common/stats/stats_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,21 @@ bool TagExtractorImpl::extractTag(const std::string& stat_name, std::vector<Tag>
}

RawStatData* HeapRawStatDataAllocator::alloc(const std::string& name) {
// This must be zero-initialized
RawStatData* data = static_cast<RawStatData*>(::calloc(RawStatData::size(), 1));
data->initialize(name);
return data;

std::unique_lock<std::mutex> lock(mutex_);

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.

Why are we allocating and then freeing on the case where we have an existing stat?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good question. Fundamentally it doesn't have to be this way, but this is an artifact of the way STL sets & maps work. STL set lookups require construction of an object. If you make this an map<string, RawStatData*> you have just pushed the problem around a little, as you'd need to copy the string to potentially truncate it, which is the same work, basically, as is being done here, and then you have to duplicate the truncation logic instead of just having it in RawStatData::initialize. Worse, you'd wind up permanently duplicating all the name storage. I argued that I don't know really how impactful that would be across different ways you might scale the system, but the current solution has zero overhead from duplication and is really no more complex from a programming perspective.

One question to ask is whether RawStatData::initialize is doing anything extra that's not required for the set lookup. It is, but it's pretty minimal and IMO not worth optimizing around.

An ideal solution would allow the set lookup against a string_view, without actually constructing the templated type. BlockMemoryHashSet::insert has that signature, so in the hot-restart case you don't need to do the prospective allocation.

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.

Yeah, fair enough. Maybe add a comment to the code capturing this design history. Thanks!

auto ret = stats_.insert(data);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

you can take the lock after the call to initialize(), to minimize the time spent holding the lock. Actually I think you can also let it go immediately after the call to insert as well as ref_count_ is atomic.

RawStatData* existing_data = *ret.first;
lock.unlock();

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.

There's a subtle problem here. The iterator you were returned can be invalidated if another element is inserted into the set. You need to grab the raw pointer from the iterator while locked, and never use the iterator again after unlocking.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh very good point Matt. Just leave it locked till the end of the function then. I can't think of why the hash implementation would need to invalidate the iterator but if the standard doesn't say it is safe then there is no point risking it.

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.

I think the lock tightening is a good idea and will probably save us some cycles. We just need to extract the raw pointer from the iterator while locked and only use the pointer temporary after we unlock.

As for how this interacts with a custom hash, my basic understanding is that as the set grows, it will at some point decide to rehash (using the same hash function) the entire set onto a larger set of buckets. This is the only process that causes iterators to be invalidated for std::unordered_set.


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

TagProducerImpl::TagProducerImpl(const envoy::config::metrics::v2::StatsConfig& config)
Expand Down Expand Up @@ -256,18 +267,26 @@ TagProducerImpl::addDefaultExtractors(const envoy::config::metrics::v2::StatsCon
}

void HeapRawStatDataAllocator::free(RawStatData& data) {
// This allocator does not ever have concurrent access to the raw data.
ASSERT(data.ref_count_ == 1);
ASSERT(data.ref_count_ > 0);
if (--data.ref_count_ > 0) {
return;
}

std::unique_lock<std::mutex> lock(mutex_);

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.

Why do we need locking at all if the old comment about "This allocator does not ever have concurrent access to the raw data" hold true?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The problem isn't the stat, it's the set.

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.

+1. Also, side note: that comment is no longer valid since the same stat can be freed/allocated multiple times, meaning that there may be cases where the allocator is operating on the same raw stat from two different threads.

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.

AFAICT we only ever do these allocations under existing locking, e.g.

SafeAllocData alloc = parent_.safeAlloc(final_name);
. Do we need to be double locking here?

I might be wrong in my assessment, please point out if not (and add a comment to the code!).

@mrice32 mrice32 May 3, 2018

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.

Yes, good point, there probably need to be comments around this. The alloc() calls are protected, but the free() calls are made from the destructors of the individual stat objects. See https://github.com/envoyproxy/envoy/blob/master/source/common/stats/stats_impl.h#L310 for an example.

size_t key_removed = stats_.erase(&data);
lock.unlock();

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

void RawStatData::initialize(absl::string_view key) {
ASSERT(!initialized());
if (key.size() > maxNameLength()) {
if (key.size() > Stats::RawStatData::maxNameLength()) {
ENVOY_LOG_MISC(
warn,
"Statistic '{}' is too long with {} characters, it will be truncated to {} characters", key,
key.size(), maxNameLength());
key.size(), Stats::RawStatData::maxNameLength());
}
ref_count_ = 1;

Expand Down
14 changes: 14 additions & 0 deletions source/common/stats/stats_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,22 @@ class HistogramImpl : public Histogram, public MetricImpl {
class HeapRawStatDataAllocator : public RawStatDataAllocator {
public:
// RawStatDataAllocator
~HeapRawStatDataAllocator() { ASSERT(stats_.empty()); }
RawStatData* alloc(const std::string& name) override;
void free(RawStatData& data) override;

private:
struct RawStatDataHash_ {
size_t operator()(const RawStatData* a) const { return HashUtil::xxHash64(a->key()); }
};
struct RawStatDataCompare_ {
bool operator()(const RawStatData* a, const RawStatData* b) const {
return (a->key() == b->key());
}
};
typedef std::unordered_set<RawStatData*, RawStatDataHash_, RawStatDataCompare_> StringRawDataSet;
StringRawDataSet stats_;

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.

Please add comments explaining what this is/does.

std::mutex mutex_;

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.

Please add comments explaining what htis protects. Ideally we use GUARDED_BY etc. macros.

};

/**
Expand Down
16 changes: 16 additions & 0 deletions test/common/stats/stats_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -478,5 +478,21 @@ TEST(RawStatDataTest, Truncate) {
alloc.free(*stat);
}

TEST(RawStatDataTest, HeapAlloc) {
HeapRawStatDataAllocator alloc;
RawStatData* stat_1 = alloc.alloc("ref_name");
ASSERT_NE(stat_1, nullptr);
RawStatData* stat_2 = alloc.alloc("ref_name");
ASSERT_NE(stat_2, nullptr);
RawStatData* stat_3 = alloc.alloc("not_ref_name");
ASSERT_NE(stat_3, nullptr);
EXPECT_EQ(stat_1, stat_2);
EXPECT_NE(stat_1, stat_3);
EXPECT_NE(stat_2, stat_3);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

let's just expect stat_3 is not nullptr too, though it looks like that would segv below anyway.

alloc.free(*stat_1);
alloc.free(*stat_2);
alloc.free(*stat_3);
}

} // namespace Stats
} // namespace Envoy