Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
98 changes: 70 additions & 28 deletions source/common/stats/thread_local_store.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ ThreadLocalStoreImpl::~ThreadLocalStoreImpl() {
ASSERT(shutting_down_ || !threading_ever_initialized_);
default_scope_.reset();
ASSERT(scopes_.empty());
ASSERT(scopes_to_cleanup_.empty());
Comment thread
jmarantz marked this conversation as resolved.
ASSERT(central_cache_entries_to_cleanup_.empty());
ASSERT(histograms_to_cleanup_.empty());
}

void ThreadLocalStoreImpl::setHistogramSettings(HistogramSettingsConstPtr&& histogram_settings) {
Expand Down Expand Up @@ -261,31 +264,48 @@ void ThreadLocalStoreImpl::releaseScopeCrossThread(ScopeImpl* scope) {
//
// Since this is called from ScopeImpl's destructor, we must bump the
// ref-count of the central-cache by copying to a local scoped pointer, and
// keep that reference alive until all the TLS caches are clear.
CentralCacheEntrySharedPtr central_cache = scope->central_cache_;
// keep that reference alive until all the TLS caches are clear. This is done by keeping a
// separate vector of shared_ptrs which will be destructed once all threads have completed.

// This can happen from any thread. We post() back to the main thread which will initiate the
// cache flush operation.
if (!shutting_down_ && main_thread_dispatcher_) {
const uint64_t scope_id = scope->scope_id_;
// Switch to batching of clearing scopes as mentioned in:
// https://gist.github.com/jmarantz/838cb6de7e74c0970ea6b63eded0139a

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.

I think we don't need to call out the gist anymore since we have incorporated the code.

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.

Will remove as I find and fix why the integration test failed in the pipeline.

// This greatly reduces the overhead of VHDS updates when there are tens of thousands of
// VirtualHosts.
bool need_post = scopes_to_cleanup_.empty();

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.

Mind including an explanation why we post when this is empty? I assume this is the batching mechanism so that we don't post while there is a post in progress, but some detail would be nice to have here

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.

will do.

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.

updating releaseScopesCrossThread to have some of the same info as releaseHistogramCrossThread. This should cover this and the additional comment below. :)

scopes_to_cleanup_.push_back(scope->scope_id_);
central_cache_entries_to_cleanup_.push_back(scope->central_cache_);
lock.release();

// TODO(jmarantz): consider batching all the scope IDs that should be
// cleared from TLS caches to reduce bursts of runOnAllThreads on a large
// config update. See the pattern below used for histograms.
main_thread_dispatcher_->post([this, central_cache, scope_id]() {
sync_.syncPoint(MainDispatcherCleanupSync);
clearScopeFromCaches(scope_id, central_cache);
});
if (need_post) {
main_thread_dispatcher_->post([this]() {
sync_.syncPoint(MainDispatcherCleanupSync);
clearScopesFromCaches();
});
}
}
}

void ThreadLocalStoreImpl::releaseHistogramCrossThread(uint64_t histogram_id) {
// This can happen from any thread. We post() back to the main thread which will initiate the
// cache flush operation.
if (!shutting_down_ && main_thread_dispatcher_) {
main_thread_dispatcher_->post(
[this, histogram_id]() { clearHistogramFromCaches(histogram_id); });
// It's possible that many different histograms will be deleted at the same
// time, before the main thread gets a chance to run
// clearHistogramsFromCaches. If a new histogram is deleted before that
// post runs, we add it to our list of histograms to clear, and there's no
// need to issue another post.
Comment on lines +311 to +313

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.

I see this is explained here, would be good to have this explanation for the above scope release :)

bool need_post = false;
{
Thread::LockGuard lock(hist_mutex_);
need_post = histograms_to_cleanup_.empty();
histograms_to_cleanup_.push_back(histogram_id);
}
if (need_post) {
main_thread_dispatcher_->post([this]() { clearHistogramsFromCaches(); });
}
}
}

Expand All @@ -294,39 +314,61 @@ ThreadLocalStoreImpl::TlsCache::insertScope(uint64_t scope_id) {
return scope_cache_[scope_id];
}

void ThreadLocalStoreImpl::TlsCache::eraseScope(uint64_t scope_id) { scope_cache_.erase(scope_id); }
void ThreadLocalStoreImpl::TlsCache::eraseHistogram(uint64_t histogram_id) {
void ThreadLocalStoreImpl::TlsCache::eraseScopes(const std::vector<uint64_t>& scope_ids) {
for (uint64_t scope_id : scope_ids) {
scope_cache_.erase(scope_id);
}
}

void ThreadLocalStoreImpl::TlsCache::eraseHistograms(const std::vector<uint64_t>& histograms) {
// This is called for every histogram in every thread, even though the
// histogram may not have been cached in each thread yet. So we don't
// want to check whether the erase() call erased anything.
tls_histogram_cache_.erase(histogram_id);
for (uint64_t histogram_id : histograms) {
tls_histogram_cache_.erase(histogram_id);
}
}

void ThreadLocalStoreImpl::clearScopeFromCaches(uint64_t scope_id,
CentralCacheEntrySharedPtr central_cache) {
void ThreadLocalStoreImpl::clearScopesFromCaches() {
// If we are shutting down we no longer perform cache flushes as workers may be shutting down
// at the same time.
if (!shutting_down_) {
// Perform a cache flush on all threads.

// Capture all the pending scope ids in a local, clearing the list held in
// this. Once this occurs, if a new scope is deleted, a new post will be
// required.
auto scope_ids = std::make_shared<std::vector<uint64_t>>();
// Capture all the central cache entries for scopes we're deleting. These will be freed after
// all threads have completed.
auto central_caches = std::make_shared<std::vector<CentralCacheEntrySharedPtr>>();
{
Thread::LockGuard lock(lock_);
scope_ids->swap(scopes_to_cleanup_);

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.

I always liked this swap() pattern by @antoniovicente has convinced me it's faster to do:

   *scope_ids = std::move(scopes_to_cleanup_);
   scopes_to_cleanup_.clear();

as there is no chance in this context that *scope_ids had anything in it. You can use that in place of all the swap()s added in this PR.

Note you do need that clear() because std::move does not strictly guarantee what state it leaves the source in, but it should be a fast no-op.

@jtway jtway Apr 8, 2021

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.

Interesting, never thought about using something like clear() to return something like this to a "known" state after std::move

@jmarantz jmarantz Apr 8, 2021

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.

Yeah I believe that is the only legal operation you can do on an object that has just had its contents moved...other than destruct it.

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 pattern used elsewhere is:
*scope_ids = std::move(scopes_to_cleanup_);
ASSERT(scopes_to_cleanup_.empty());

// post_callbacks_ should be empty after the move.

central_caches->swap(central_cache_entries_to_cleanup_);
}

tls_cache_->runOnAllThreads(
[scope_id](OptRef<TlsCache> tls_cache) { tls_cache->eraseScope(scope_id); },
[central_cache]() { /* Holds onto central_cache until all tls caches are clear */ });
[scope_ids](OptRef<TlsCache> tls_cache) { tls_cache->eraseScopes(*scope_ids); },
[central_caches]() { /* Holds onto central_caches until all tls caches are clear */ });
}
}

void ThreadLocalStoreImpl::clearHistogramFromCaches(uint64_t histogram_id) {
void ThreadLocalStoreImpl::clearHistogramsFromCaches() {
// If we are shutting down we no longer perform cache flushes as workers may be shutting down
// at the same time.
if (!shutting_down_) {
// Perform a cache flush on all threads.
//
// TODO(jmarantz): If this cross-thread posting proves to be a performance
// bottleneck,
// https://gist.github.com/jmarantz/838cb6de7e74c0970ea6b63eded0139a
// contains a patch that will implement batching together to clear multiple
// histograms.
// Capture all the pending histograms in a local, clearing the list held in
// this. Once this occurs, if a new histogram is deleted, a new post will be

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.

This comment is a bit hard to read because talking about this makes the sentence seem incomplete. Maybe rephrase? Something like Move the histograms pending cleanup into a local variable. Future histogram deletions will be batched until the next time this function is called.?

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.

Will reword

// required.
auto histograms = std::make_shared<std::vector<uint64_t>>();
{
Thread::LockGuard lock(hist_mutex_);
histograms->swap(histograms_to_cleanup_);
}

tls_cache_->runOnAllThreads(
[histogram_id](OptRef<TlsCache> tls_cache) { tls_cache->eraseHistogram(histogram_id); });
[histograms](OptRef<TlsCache> tls_cache) { tls_cache->eraseHistograms(*histograms); });
}
}

Expand Down
21 changes: 17 additions & 4 deletions source/common/stats/thread_local_store.h
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,8 @@ class ThreadLocalStoreImpl : Logger::Loggable<Logger::Id::stats>, public StoreRo

struct TlsCache : public ThreadLocal::ThreadLocalObject {
TlsCacheEntry& insertScope(uint64_t scope_id);
void eraseScope(uint64_t scope_id);
void eraseHistogram(uint64_t histogram);
void eraseScopes(const std::vector<uint64_t>& scope_ids);
void eraseHistograms(const std::vector<uint64_t>& histograms);

// The TLS scope cache is keyed by scope ID. This is used to avoid complex circular references
// during scope destruction. An ID is required vs. using the address of the scope pointer
Expand All @@ -472,8 +472,8 @@ class ThreadLocalStoreImpl : Logger::Loggable<Logger::Id::stats>, public StoreRo
}

std::string getTagsForName(const std::string& name, TagVector& tags) const;
void clearScopeFromCaches(uint64_t scope_id, CentralCacheEntrySharedPtr central_cache);
void clearHistogramFromCaches(uint64_t histogram_id);
void clearScopesFromCaches();
void clearHistogramsFromCaches();
void releaseScopeCrossThread(ScopeImpl* scope);
void mergeInternal(PostMergeCb merge_cb);
bool rejects(StatName name) const;
Expand Down Expand Up @@ -526,6 +526,19 @@ class ThreadLocalStoreImpl : Logger::Loggable<Logger::Id::stats>, public StoreRo
std::vector<GaugeSharedPtr> deleted_gauges_ ABSL_GUARDED_BY(lock_);
std::vector<HistogramSharedPtr> deleted_histograms_ ABSL_GUARDED_BY(lock_);
std::vector<TextReadoutSharedPtr> deleted_text_readouts_ ABSL_GUARDED_BY(lock_);

// Scope IDs and central cache entries that are queued for cross-scope release.
// Because there can be a large number of scopes, all of which are released at once,
// (e.g. when a scope is deleted), it is more efficient to batch their cleanup,
// which would otherwise entail a post() per scope per thread.
std::vector<uint64_t> scopes_to_cleanup_ ABSL_GUARDED_BY(lock_);
std::vector<CentralCacheEntrySharedPtr> central_cache_entries_to_cleanup_ ABSL_GUARDED_BY(lock_);

// Histograms IDs that are queued for cross-scope release. Because there
// can be a large number of histograms, all of which are released at once,
// (e.g. when a scope is deleted), it is likely more efficient to batch their
// cleanup, which would otherwise entail a post() per histogram per thread.
std::vector<uint64_t> histograms_to_cleanup_ ABSL_GUARDED_BY(hist_mutex_);
};

using ThreadLocalStoreImplPtr = std::unique_ptr<ThreadLocalStoreImpl>;
Expand Down