Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5c29ac0
add API to do a symbolic prefix match on StatNames without taking a l…
jmarantz Jun 16, 2021
b134c93
add prefix-optimized stats_matcher impl.
jmarantz Jun 17, 2021
68b7d41
Use the new stats-matcher API that optikmizes for prefixes.
jmarantz Jun 17, 2021
f5068cc
Skip remembering rejected stats when specified cheaply.
jmarantz Jun 17, 2021
bb11ad9
add unit tests showing memory benefit, and benchmark tests showing CP…
jmarantz Jun 17, 2021
7dc0ff0
Merge branch 'main' into stat-name-prefix-match
jmarantz Jun 17, 2021
b6a4edb
add missing arg.
jmarantz Jun 18, 2021
c7a1147
NOLINT for clang-tidy nits
jmarantz Jun 18, 2021
2dd3d57
short-circuit more code that doesn't need to run for stats that are r…
jmarantz Jun 18, 2021
d338dae
Merge branch 'main' into stat-name-prefix-match
jmarantz Jun 18, 2021
8add54b
Fix spelling and simplify the comment.
jmarantz Jun 18, 2021
bfd6241
cleanup and add tests.
jmarantz Jun 19, 2021
b738222
Remove StatMatcher::hasStringMatchers which is no longer needed.
jmarantz Jun 19, 2021
051f412
improve commenting
jmarantz Jun 20, 2021
b1d572d
Merge branch 'main' into stat-name-prefix-match
jmarantz Jun 20, 2021
7dd1905
Merge branch 'main' into stat-name-prefix-match
jmarantz Jul 1, 2021
1ff784f
introduce an intermediate result resulted by fastRejects() and requir…
jmarantz Jul 2, 2021
5d4668f
Merge branch 'main' into stat-name-prefix-match
jmarantz Jul 6, 2021
60e8c00
Cleanup comments.
jmarantz Jul 6, 2021
fa7c3ca
Switch from a class to an enum for FastResult; it's a bit cleaner.
jmarantz Jul 7, 2021
8dd325e
fast-reject the RejectsAll case, and test for that.
jmarantz Jul 8, 2021
2b21989
Merge branch 'main' into stat-name-prefix-match
jmarantz Jul 8, 2021
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: 29 additions & 2 deletions envoy/stats/stats_matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,43 @@
namespace Envoy {
namespace Stats {

class StatName;

class StatsMatcher {
public:
virtual ~StatsMatcher() = default;

/**
* Take a metric name and report whether or not it should be instantiated.
* @param the name of a Stats::Metric.
* The may need to convert the StatName to a string.
Comment thread
jmarantz marked this conversation as resolved.
Outdated
*
* @param name the name of a Stats::Metric.
* @return bool true if that stat should not be instantiated.
*/
virtual bool rejects(const std::string& name) const PURE;
virtual bool rejects(StatName name) const PURE;

/**
* Takes a metric name and quickly determine whether it can be rejected based
Comment thread
jmarantz marked this conversation as resolved.
Outdated
* purely on the StatName. A return of 'false' means we will need to check
* slowRejects as well.
*
* @param name the name of a Stats::Metric.
* @return bool true if that stat should not be instantiated, or whether we
* need to check slowRejects.
*/
virtual bool fastRejects(StatName name) const PURE;

/**
* Takes a metric name and converts it to a string, if needed, to determine
* whether it needs to be rejected. This is intended to be used if fastRejects()
* returns false. It is a good idea to cache the results of this, to avoid the
* stringification overhead as well as a global symbol table lock.
*
* @param name the name of a Stats::Metric.
* @return bool true if that stat should not be instantiated, or whether we
* need to check slowRejects.
*/
virtual bool slowRejects(StatName name) const PURE;

/**
* Helps determine whether the matcher needs to be called. This can be used
Expand Down
10 changes: 10 additions & 0 deletions source/common/common/matchers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ bool StringMatcherImpl::match(const absl::string_view value) const {
}
}

bool StringMatcherImpl::getCaseSensitivePrefixMatch(std::string& prefix) const {
if (matcher_.match_pattern_case() ==
envoy::type::matcher::v3::StringMatcher::MatchPatternCase::kPrefix &&
!matcher_.ignore_case()) {
prefix = matcher_.prefix();
return true;
}
return false;
}

ListMatcher::ListMatcher(const envoy::type::matcher::v3::ListMatcher& matcher) : matcher_(matcher) {
ASSERT(matcher_.match_pattern_case() ==
envoy::type::matcher::v3::ListMatcher::MatchPatternCase::kOneOf);
Expand Down
10 changes: 10 additions & 0 deletions source/common/common/matchers.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,21 @@ class StringMatcherImpl : public ValueMatcher, public StringMatcher {
public:
explicit StringMatcherImpl(const envoy::type::matcher::v3::StringMatcher& matcher);

// StringMatcher
bool match(const absl::string_view value) const override;
bool match(const ProtobufWkt::Value& value) const override;

const envoy::type::matcher::v3::StringMatcher& matcher() const { return matcher_; }

/**
* Helps applications optimize the case where a matcher is a case-sensitive
* prefix-match.
*
* @param prefix the returned prefix string
* @return true if the matcher is a case-sensitive prefix-match.
*/
bool getCaseSensitivePrefixMatch(std::string& prefix) const;

private:
const envoy::type::matcher::v3::StringMatcher matcher_;
Regex::CompiledMatcherPtr regex_;
Expand Down
5 changes: 3 additions & 2 deletions source/common/config/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,9 @@ Utility::createTagProducer(const envoy::config::bootstrap::v3::Bootstrap& bootst
}

Stats::StatsMatcherPtr
Utility::createStatsMatcher(const envoy::config::bootstrap::v3::Bootstrap& bootstrap) {
return std::make_unique<Stats::StatsMatcherImpl>(bootstrap.stats_config());
Utility::createStatsMatcher(const envoy::config::bootstrap::v3::Bootstrap& bootstrap,
Stats::SymbolTable& symbol_table) {
return std::make_unique<Stats::StatsMatcherImpl>(bootstrap.stats_config(), symbol_table);
}

Stats::HistogramSettingsConstPtr
Expand Down
3 changes: 2 additions & 1 deletion source/common/config/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,8 @@ class Utility {
* Create StatsMatcher instance.
*/
static Stats::StatsMatcherPtr
createStatsMatcher(const envoy::config::bootstrap::v3::Bootstrap& bootstrap);
createStatsMatcher(const envoy::config::bootstrap::v3::Bootstrap& bootstrap,
Stats::SymbolTable& symbol_table);

/**
* Create HistogramSettings instance.
Expand Down
1 change: 1 addition & 0 deletions source/common/stats/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ envoy_cc_library(
srcs = ["stats_matcher_impl.cc"],
hdrs = ["stats_matcher_impl.h"],
deps = [
":symbol_table_lib",
"//envoy/stats:stats_interface",
"//source/common/common:matchers_lib",
"//source/common/protobuf",
Expand Down
79 changes: 74 additions & 5 deletions source/common/stats/stats_matcher_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@

#include "source/common/common/utility.h"

#include "absl/strings/match.h"

namespace Envoy {
namespace Stats {

// TODO(ambuc): Refactor this into common/matchers.cc, since StatsMatcher is really just a thin
// wrapper around what might be called a StringMatcherList.
StatsMatcherImpl::StatsMatcherImpl(const envoy::config::metrics::v3::StatsConfig& config) {
StatsMatcherImpl::StatsMatcherImpl(const envoy::config::metrics::v3::StatsConfig& config,
SymbolTable& symbol_table)
: symbol_table_(symbol_table), stat_name_pool_(std::make_unique<StatNamePool>(symbol_table)) {

switch (config.stats_matcher().stats_matcher_case()) {
case envoy::config::metrics::v3::StatsMatcher::StatsMatcherCase::kRejectAll:
// In this scenario, there are no matchers to store.
Expand All @@ -22,13 +27,15 @@ StatsMatcherImpl::StatsMatcherImpl(const envoy::config::metrics::v3::StatsConfig
// If we have an inclusion list, we are being default-exclusive.
for (const auto& stats_matcher : config.stats_matcher().inclusion_list().patterns()) {
matchers_.push_back(Matchers::StringMatcherImpl(stats_matcher));
optimizeLastMatcher();
}
is_inclusive_ = false;
break;
case envoy::config::metrics::v3::StatsMatcher::StatsMatcherCase::kExclusionList:
// If we have an exclusion list, we are being default-inclusive.
for (const auto& stats_matcher : config.stats_matcher().exclusion_list().patterns()) {
matchers_.push_back(Matchers::StringMatcherImpl(stats_matcher));
optimizeLastMatcher();
}
FALLTHRU;
default:
Expand All @@ -38,8 +45,55 @@ StatsMatcherImpl::StatsMatcherImpl(const envoy::config::metrics::v3::StatsConfig
}
}

bool StatsMatcherImpl::rejects(const std::string& name) const {
//
// If the last string-matcher added is a case-sensitive prefix match, and the
// prefix ends in ".", then this drops that match and adds it to a list of
// prefixes. This is beneficial because token prefixes can be handled more
// efficiently as a StatName without requiring conversion to a string.
//
// In the future, other matcher patterns could be optimized in a similar way,
// such as:
// * suffixes that begin with "."
// * exact-matches
// * substrings that begin and end with "."
//
// These are left unoptimized for the moment to keep the code-change simpler,
// and because we haven't observed an acute performance need to optimize those
// other patterns yet.
void StatsMatcherImpl::optimizeLastMatcher() {
std::string prefix;
if (matchers_.back().getCaseSensitivePrefixMatch(prefix) && absl::EndsWith(prefix, ".") &&
prefix.size() > 1) {
prefixes_.push_back(stat_name_pool_->add(prefix.substr(0, prefix.size() - 1)));
matchers_.pop_back();
}
}

bool StatsMatcherImpl::fastRejects(StatName stat_name) const {
if (rejectsAll()) {
return true;
}

// We can short-circuit the slow matchers only if they are empty, or if
// we are in inclusive-mode and we find a match.
if (is_inclusive_ || matchers_.empty()) {
return fastRejectMatch(stat_name) == is_inclusive_;
}

// If there are both prefix and string matchers, and we are in exclusive
// mode, then it isn't helpful to run the prefix matchers early, so
// we return false. This forces the caller to eventually then call
// slowRejects(), where we'll handle this case below.
return false;
}

bool StatsMatcherImpl::fastRejectMatch(StatName stat_name) const {
return std::any_of(prefixes_.begin(), prefixes_.end(),
[stat_name](StatName prefix) { return stat_name.startsWith(prefix); });
}

bool StatsMatcherImpl::slowRejects(StatName stat_name) const {
bool match = slowRejectMatch(stat_name);

// is_inclusive_ | match | return
// ---------------+-------+--------
// T | T | T Default-inclusive and matching an (exclusion) matcher, deny.
Expand All @@ -49,8 +103,23 @@ bool StatsMatcherImpl::rejects(const std::string& name) const {
//
// This is an XNOR, which can be evaluated by checking for equality.

return (is_inclusive_ == std::any_of(matchers_.begin(), matchers_.end(),
[&name](auto& matcher) { return matcher.match(name); }));
if (is_inclusive_ || match || prefixes_.empty()) {
return match == is_inclusive_;
}

// In exclusive-mode with both prefixes and matches, we must
// skip the fast-path prefix since we invert the results, so we can't
// definitively declare a rejection. So we must check them now.
return !fastRejectMatch(stat_name);
}

bool StatsMatcherImpl::slowRejectMatch(StatName stat_name) const {
if (matchers_.empty()) {
return false;
}
std::string name = symbol_table_->toString(stat_name);
return std::any_of(matchers_.begin(), matchers_.end(),
[&name](auto& matcher) { return matcher.match(name); });
}

} // namespace Stats
Expand Down
25 changes: 21 additions & 4 deletions source/common/stats/stats_matcher_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

#include <string>

#include "envoy/common/optref.h"
#include "envoy/config/metrics/v3/stats.pb.h"
#include "envoy/stats/stats_matcher.h"

#include "source/common/common/matchers.h"
#include "source/common/protobuf/protobuf.h"
#include "source/common/stats/symbol_table_impl.h"

#include "absl/strings/string_view.h"

Expand All @@ -18,22 +20,37 @@ namespace Stats {
*/
class StatsMatcherImpl : public StatsMatcher {
public:
explicit StatsMatcherImpl(const envoy::config::metrics::v3::StatsConfig& config);
StatsMatcherImpl(const envoy::config::metrics::v3::StatsConfig& config,
SymbolTable& symbol_table);

// Default constructor simply allows everything.
StatsMatcherImpl() = default;

// StatsMatcher
bool rejects(const std::string& name) const override;
bool acceptsAll() const override { return is_inclusive_ && matchers_.empty(); }
bool rejectsAll() const override { return !is_inclusive_ && matchers_.empty(); }
bool rejects(StatName name) const override { return fastRejects(name) || slowRejects(name); }
bool fastRejects(StatName name) const override;
bool slowRejects(StatName name) const override;
bool acceptsAll() const override {
return is_inclusive_ && matchers_.empty() && prefixes_.empty();
}
bool rejectsAll() const override {
return !is_inclusive_ && matchers_.empty() && prefixes_.empty();
}

private:
void optimizeLastMatcher();
bool fastRejectMatch(StatName name) const;
bool slowRejectMatch(StatName name) const;

// Bool indicating whether or not the StatsMatcher is including or excluding stats by default. See
// StatsMatcherImpl::rejects() for much more detail.
bool is_inclusive_{true};

OptRef<SymbolTable> symbol_table_;
std::unique_ptr<StatNamePool> stat_name_pool_;

std::vector<Matchers::StringMatcherImpl> matchers_;
std::vector<StatName> prefixes_;
};

} // namespace Stats
Expand Down
37 changes: 37 additions & 0 deletions source/common/stats/symbol_table_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,43 @@ void SymbolTableImpl::Encoding::decodeTokens(
}
}

bool StatName::startsWith(StatName symbolic_prefix) const {
bool ret = true;
std::vector<Symbol> prefix_symbols;

// Decode the prefix as a StatNameVec.
SymbolTableImpl::Encoding::decodeTokens(
symbolic_prefix.data(), symbolic_prefix.dataSize(),
[&prefix_symbols](Symbol symbol) { prefix_symbols.push_back(symbol); },
[&ret](absl::string_view) { ret = false; });

// If there any dynamic components, our string_view lambda will be called, and
// then we'll return false for simplicity. We don't have a current need for
// prefixes to be expressed dynamically, and handling that case would add
// complexity.
if (!ret) {
return false;
}

// Now decode the StatName, matching against the symbols. It's OK for there to
// be dynamic string_view elements after the prefix match.
uint32_t index = 0;
SymbolTableImpl::Encoding::decodeTokens(
data(), dataSize(),
[&prefix_symbols, &index, &ret](Symbol symbol) {
if (index < prefix_symbols.size() && prefix_symbols[index] != symbol) {
ret = false;
}
++index;
},
[&prefix_symbols, &index, &ret](absl::string_view) {
if (index < prefix_symbols.size()) {
ret = false;
}
});
return ret && index >= prefix_symbols.size();
}

std::vector<absl::string_view> SymbolTableImpl::decodeStrings(const SymbolTable::Storage array,
size_t size) const {
std::vector<absl::string_view> strings;
Expand Down
10 changes: 10 additions & 0 deletions source/common/stats/symbol_table_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,16 @@ class StatName {
*/
bool empty() const { return size_and_data_ == nullptr || dataSize() == 0; }

/**
* Determines whether this starts with the prefix. Note: dynamic segments
* are not supported in the current implementation; this matching only works
* for symbolic segments. However it OK for this to include dynamic segments
* following the prefix.
*
* @param symbolic_prefix the prefix, which must not contain dynamic segments.
*/
bool startsWith(StatName symbolic_prefix) const;

private:
/**
* Casts the raw data as a string_view. Note that this string_view will not
Expand Down
Loading