summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMilena Ivanova <milena.ivanova@mongodb.com>2021-05-28 09:24:11 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2021-08-05 08:24:59 +0000
commit98b4a0a2b2c4a230efe1c2ddc3da487a575d670d (patch)
tree7269ee48a69bfaea61b77a8d3f78bc072735b3b0
parent85976973ce08a8d7dedbcc108d4ace00c25c9b04 (diff)
downloadmongo-98b4a0a2b2c4a230efe1c2ddc3da487a575d670d.tar.gz
SERVER-56602 Track usage of match expressions in serverStatus
-rw-r--r--jstests/noPassthrough/operator_counters_match.js289
-rw-r--r--src/mongo/db/commands/find_cmd.cpp2
-rw-r--r--src/mongo/db/commands/run_aggregate.cpp2
-rw-r--r--src/mongo/db/curop.h11
-rw-r--r--src/mongo/db/matcher/SConscript1
-rw-r--r--src/mongo/db/matcher/expression_leaf.cpp28
-rw-r--r--src/mongo/db/matcher/expression_leaf.h2
-rw-r--r--src/mongo/db/matcher/expression_parser.cpp39
-rw-r--r--src/mongo/db/pipeline/expression_context.cpp21
-rw-r--r--src/mongo/db/pipeline/expression_context.h28
-rw-r--r--src/mongo/db/query/canonical_query.cpp5
-rw-r--r--src/mongo/db/stats/counters.cpp1
-rw-r--r--src/mongo/db/stats/counters.h33
13 files changed, 445 insertions, 17 deletions
diff --git a/jstests/noPassthrough/operator_counters_match.js b/jstests/noPassthrough/operator_counters_match.js
new file mode 100644
index 00000000000..f1f18222dfe
--- /dev/null
+++ b/jstests/noPassthrough/operator_counters_match.js
@@ -0,0 +1,289 @@
+/**
+ * Tests counters for match expressions.
+ * @tags: [requires_fcv_50]
+ */
+
+(function() {
+"use strict";
+const mongod = MongoRunner.runMongod();
+const db = mongod.getDB(jsTest.name());
+const collName = jsTest.name();
+const coll = db[collName];
+coll.drop();
+
+assert.commandWorked(coll.insert({_id: 0, a: 0, b: "foo", c: [10, 50]}));
+assert.commandWorked(coll.insert({_id: 1, a: 1, b: "foo", c: [10, 20, 30]}));
+assert.commandWorked(coll.insert({_id: 2, a: 2, b: "bar"}));
+assert.commandWorked(coll.insert({_id: 3, a: 3, c: [20]}));
+
+/**
+ * Run the command and check that the specified match expression counters are increased by the given
+ * value.
+ * - countersToIncrease: document with elements <match expression> : < increment value>.
+ */
+function checkCountersWithValues(command, countersToIncrease) {
+ let beforeMetrics = {};
+ const metrics = db.serverStatus().metrics.operatorCounters.match;
+ for (let [exprName, inc] of Object.entries(countersToIncrease)) {
+ beforeMetrics[exprName] = metrics[exprName];
+ }
+
+ command();
+
+ const afterMetrics = db.serverStatus().metrics.operatorCounters.match;
+ for (let [exprName, inc] of Object.entries(countersToIncrease)) {
+ assert.eq(afterMetrics[exprName], beforeMetrics[exprName] + inc, exprName);
+ }
+}
+
+/**
+ * Simplified version of the above function where the counters are expected to increase by 1.
+ * - countersToIncrease: array of match expression names or a single expression name.
+ */
+function checkCounters(command, countersToIncrease) {
+ if (!Array.isArray(countersToIncrease)) {
+ countersToIncrease = [countersToIncrease];
+ }
+ checkCountersWithValues(command, countersToIncrease.reduce((acc, val) => {
+ acc[val] = 1;
+ return acc;
+ }, {}));
+}
+
+/**
+ * Run a command expected to fail and check that the specified match expression counters are not
+ * increased.
+ * - countersNotToIncrease: array of match expression names.
+ */
+function checkCountersWithError(command, errorCode, countersNotToIncrease) {
+ let beforeMetrics = {};
+ const metrics = db.serverStatus().metrics.operatorCounters.match;
+ countersNotToIncrease.forEach((exprName) => {
+ beforeMetrics[exprName] = metrics[exprName];
+ });
+
+ const error = assert.throws(command);
+ assert.commandFailedWithCode(error, errorCode);
+
+ const afterMetrics = db.serverStatus().metrics.operatorCounters.match;
+ countersNotToIncrease.forEach((exprName) => {
+ assert.eq(afterMetrics[exprName], beforeMetrics[exprName], exprName);
+ });
+}
+
+// Match expression counters in find.
+checkCounters(() => assert.eq(1, coll.find({a: 1}).itcount()), "$eq");
+
+checkCounters(() => assert.eq(2, coll.find({b: {$eq: "foo"}}).itcount()), "$eq");
+
+checkCounters(() => assert.eq(2, coll.find({a: {$gt: 1}}).itcount()), "$gt");
+
+checkCounters(() => assert.eq(3, coll.find({a: {$gte: 1}}).itcount()), "$gte");
+
+checkCounters(() => assert.eq(1, coll.find({a: {$lt: 1}}).itcount()), "$lt");
+
+checkCounters(() => assert.eq(2, coll.find({a: {$lte: 1}}).itcount()), "$lte");
+
+checkCounters(() => assert.eq(3, coll.find({a: {$ne: 1}}).itcount()), "$ne");
+
+checkCounters(() => assert.eq(2, coll.find({a: {$in: [1, 2]}}).itcount()), "$in");
+
+checkCounters(() => assert.eq(2, coll.find({a: {$nin: [0, 2]}}).itcount()), "$nin");
+
+checkCounters(() => assert.eq(1, coll.find({a: {$gt: 1, $in: [1, 2]}}).itcount()), ["$in", "$gt"]);
+
+checkCounters(() => assert.eq(3, coll.find({c: {$exists: true}}).itcount()), "$exists");
+
+checkCounters(() => assert.eq(4, coll.find({a: {$type: 1}}).itcount()), "$type");
+
+checkCounters(() => assert.eq(2, coll.find({a: {$mod: [2, 0]}}).itcount()), "$mod");
+
+checkCounters(() => assert.eq(2, coll.find({b: {$regex: /^f/}}).itcount()), "$regex");
+
+checkCounters(() => assert.eq(2, coll.find({b: /^f/}).itcount()), "$regex");
+
+checkCounters(() => assert.eq(2, coll.find({b: {$not: /^f/}}).itcount()), ["$regex", "$not"]);
+
+checkCounters(() => assert.eq(2, coll.find({$jsonSchema: {required: ["a", "b", "c"]}}).itcount()),
+ "$jsonSchema");
+
+checkCounters(() => assert.eq(4, coll.find({$alwaysTrue: 1}).itcount()), "$alwaysTrue");
+
+checkCounters(() => assert.eq(0, coll.find({$alwaysFalse: 1}).itcount()), "$alwaysFalse");
+
+// Increments only the $expr counter, counters for $rand, $lt, and $add are recorded in
+// operatorCounters.expressions metrics.
+checkCounters(
+ () => assert.eq(4, coll.find({$expr: {$lt: ["$a", {$add: [{$rand: {}}, 10]}]}}).itcount()),
+ "$expr");
+
+checkCounters(
+ () => assert.eq(2, coll.find({a: {$mod: [2, 0]}, $comment: "Find even values."}).itcount()),
+ ["$mod", "$comment"]);
+
+checkCounters(() => assert.eq(1,
+ coll.find({
+ $where: function() {
+ return (this.a == 1);
+ }
+ })
+ .itcount()),
+ "$where");
+
+// Array query operators.
+checkCounters(() => assert.eq(2, coll.find({c: {$elemMatch: {$gt: 10, $lt: 50}}}).itcount()),
+ ["$elemMatch", "$gt", "$lt"]);
+
+checkCounters(() => assert.eq(1, coll.find({c: {$size: 2}}).itcount()), "$size");
+
+checkCounters(() => assert.eq(1, coll.find({c: {$all: [10, 20]}}).itcount()), "$all");
+
+// Logical operators.
+checkCountersWithValues(() => assert.eq(1, coll.find({$and: [{c: 10}, {c: 20}]}).itcount()),
+ {"$and": 1, "$eq": 2});
+
+checkCounters(() => assert.eq(2, coll.find({$and: [{c: 10}, {a: {$lt: 2}}]}).itcount()),
+ ["$and", "$lt", "$eq"]);
+
+checkCountersWithValues(() => assert.eq(2, coll.find({$or: [{c: 50}, {a: 2}]}).itcount()),
+ {"$or": 1, "$eq": 2});
+
+checkCounters(() => assert.eq(1, coll.find({$nor: [{a: {$gt: 1}}, {c: 50}]}).itcount()),
+ ["$nor", "$eq", "$gt"]);
+
+checkCounters(() => assert.eq(2, coll.find({b: {$not: {$eq: "foo"}}}).itcount()), ["$not", "$eq"]);
+
+// Bitwise query operators.
+checkCounters(() => assert.eq(2, coll.find({a: {$bitsAllClear: [0]}}).itcount()), "$bitsAllClear");
+
+checkCounters(() => assert.eq(1, coll.find({a: {$bitsAllSet: [0, 1]}}).itcount()), "$bitsAllSet");
+
+checkCounters(() => assert.eq(3, coll.find({a: {$bitsAnyClear: [0, 1]}}).itcount()),
+ "$bitsAnyClear");
+
+checkCounters(() => assert.eq(3, coll.find({a: {$bitsAnySet: [0, 1]}}).itcount()), "$bitsAnySet");
+
+// Invalid expressions do not increment any counter.
+checkCountersWithError(() => coll.find({$or: [{c: {$size: 'a'}}, {a: 2}]}).itcount(),
+ ErrorCodes.BadValue,
+ ["$or", "$eq", "$size"]);
+
+// Match expression counters in aggregation pipelines.
+
+let pipeline = [{$match: {_id: 1}}];
+checkCounters(() => assert.eq(1, coll.aggregate(pipeline).itcount()), "$eq");
+
+pipeline = [{$match: {_id: {$gt: 0}}}];
+checkCounters(() => assert.eq(3, coll.aggregate(pipeline).itcount()), "$gt");
+
+pipeline = [{$match: {$and: [{c: 10}, {a: {$lt: 2}}]}}];
+checkCounters(() => assert.eq(2, coll.aggregate(pipeline).itcount()), ["$and", "$eq", "$lt"]);
+
+pipeline = [{$match: {c: {$type: 4}}}];
+checkCounters(() => assert.eq(3, coll.aggregate(pipeline).itcount()), "$type");
+
+pipeline = [{$match: {c: {$exists: false}}}];
+checkCounters(() => assert.eq(1, coll.aggregate(pipeline).itcount()), "$exists");
+
+pipeline = [{$match: {c: {$size: 1}}}];
+checkCounters(() => assert.eq(1, coll.aggregate(pipeline).itcount()), "$size");
+
+pipeline = [{$match: {a: {$bitsAllSet: [0, 1]}}}];
+checkCounters(() => assert.eq(1, coll.aggregate(pipeline).itcount()), "$bitsAllSet");
+
+pipeline = [{$match: {$sampleRate: 0.25}}];
+checkCounters(() => assert.lte(0, coll.aggregate(pipeline).itcount()), "$sampleRate");
+
+// Invalid expressions do not increment any counter.
+checkCountersWithError(
+ () => coll.aggregate([{$match: {$and: [{b: /^fo/}, {a: {$mod: [2, 'z']}}]}}]).itcount(),
+ ErrorCodes.BadValue,
+ ["$and", "$regex", "$mod"]);
+
+// Text search.
+const textCollName = "myTextCollection";
+const textColl = db[textCollName];
+
+textColl.drop();
+assert.commandWorked(textColl.insert({_id: 0, title: "coffee"}));
+assert.commandWorked(textColl.insert({_id: 1, title: "Bake a cake"}));
+assert.commandWorked(textColl.insert({_id: 2, title: "Cake with coffee"}));
+
+assert.commandWorked(textColl.createIndex({title: "text"}));
+
+checkCounters(
+ () => assert.eq(1, textColl.find({$text: {$search: "cake", $caseSensitive: true}}).itcount()),
+ "$text");
+
+// Geospatial expressions.
+const geoCollName = "myGeoCollection";
+const geoColl = db[geoCollName];
+
+geoColl.drop();
+assert.commandWorked(geoColl.insert({_id: 0, location: {type: "Point", coordinates: [10, 20]}}));
+assert.commandWorked(geoColl.createIndex({location: "2dsphere"}));
+
+checkCounters(() => assert.eq(1,
+ geoColl
+ .find({
+ location: {
+ $near: {
+ $geometry: {type: "Point", coordinates: [10, 20]},
+ $minDistance: 0,
+ $maxDistance: 100
+ }
+ }
+ })
+ .itcount()),
+ "$near");
+
+checkCounters(() => assert.eq(1,
+ geoColl
+ .find({
+ location: {
+ $nearSphere: {
+ $geometry: {type: "Point", coordinates: [10.001, 20]},
+ $minDistance: 100,
+ $maxDistance: 500
+ }
+ }
+ })
+ .itcount()),
+ "$nearSphere");
+
+checkCounters(
+ () => assert.eq(
+ 1,
+ geoColl
+ .find({
+ location: {
+ $geoWithin: {
+ $geometry: {
+ type: "Polygon",
+ coordinates: [[[12, 18], [12, 21], [8, 21], [8, 18], [12, 18]]]
+ },
+ }
+ }
+ })
+ .itcount()),
+ "$geoWithin");
+
+checkCounters(
+ () => assert.eq(
+ 1,
+ geoColl
+ .find({
+ location: {
+ $geoIntersects: {
+ $geometry: {
+ type: "Polygon",
+ coordinates: [[[12, 18], [12, 21], [8, 21], [8, 18], [12, 18]]]
+ },
+ }
+ }
+ })
+ .itcount()),
+ "$geoIntersects");
+
+MongoRunner.stopMongod(mongod);
+})();
diff --git a/src/mongo/db/commands/find_cmd.cpp b/src/mongo/db/commands/find_cmd.cpp
index 9dd3393360e..31faa7c7bb3 100644
--- a/src/mongo/db/commands/find_cmd.cpp
+++ b/src/mongo/db/commands/find_cmd.cpp
@@ -158,6 +158,8 @@ boost::intrusive_ptr<ExpressionContext> makeExpressionContext(
CurOp::get(opCtx)->dbProfileLevel() > 0 // mayDbProfile
);
expCtx->tempDir = storageGlobalParams.dbpath + "/_tmp";
+ expCtx->startExpressionCounters();
+
return expCtx;
}
diff --git a/src/mongo/db/commands/run_aggregate.cpp b/src/mongo/db/commands/run_aggregate.cpp
index 789581c7337..a25075c2839 100644
--- a/src/mongo/db/commands/run_aggregate.cpp
+++ b/src/mongo/db/commands/run_aggregate.cpp
@@ -764,7 +764,9 @@ Status runAggregate(OperationContext* opCtx,
expCtx = makeExpressionContext(
opCtx, request, std::move(*collatorToUse), uuid, collatorToUseMatchesDefault);
+ expCtx->startExpressionCounters();
auto pipeline = Pipeline::parse(request.getPipeline(), expCtx);
+ expCtx->stopExpressionCounters();
// Check that the view's collation matches the collation of any views involved in the
// pipeline.
diff --git a/src/mongo/db/curop.h b/src/mongo/db/curop.h
index 8c3adec3a91..afb48de2440 100644
--- a/src/mongo/db/curop.h
+++ b/src/mongo/db/curop.h
@@ -775,6 +775,16 @@ public:
_tickSource = tickSource;
}
+ /**
+ * Merge match counters from the current operation into the global map and stop counting.
+ */
+ void stopMatchExprCounter();
+
+ /**
+ * Increment the counter for the match expression with given name in the current operation.
+ */
+ void incrementMatchExprCounter(StringData name);
+
private:
class CurOpStack;
@@ -846,4 +856,5 @@ private:
TickSource* _tickSource = nullptr;
};
+
} // namespace mongo
diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript
index aa1badd3cb7..f7b42816cb8 100644
--- a/src/mongo/db/matcher/SConscript
+++ b/src/mongo/db/matcher/SConscript
@@ -70,6 +70,7 @@ env.Library(
'$BUILD_DIR/mongo/db/pipeline/expression_context',
'$BUILD_DIR/mongo/db/query/collation/collator_interface',
'$BUILD_DIR/mongo/db/query/query_knobs',
+ '$BUILD_DIR/mongo/db/stats/counters',
'$BUILD_DIR/mongo/idl/idl_parser',
'$BUILD_DIR/mongo/util/regex_util',
'$BUILD_DIR/third_party/shim_pcrecpp',
diff --git a/src/mongo/db/matcher/expression_leaf.cpp b/src/mongo/db/matcher/expression_leaf.cpp
index c1f9a9ebb44..8f000b52728 100644
--- a/src/mongo/db/matcher/expression_leaf.cpp
+++ b/src/mongo/db/matcher/expression_leaf.cpp
@@ -778,29 +778,29 @@ bool BitTestMatchExpression::matchesSingleElement(const BSONElement& e,
return performBitTest(eValue);
}
-void BitTestMatchExpression::debugString(StringBuilder& debug, int indentationLevel) const {
- _debugAddSpace(debug, indentationLevel);
-
- debug << path() << " ";
-
+std::string BitTestMatchExpression::name() const {
switch (matchType()) {
case BITS_ALL_SET:
- debug << "$bitsAllSet:";
- break;
+ return "$bitsAllSet";
+
case BITS_ALL_CLEAR:
- debug << "$bitsAllClear:";
- break;
+ return "$bitsAllClear";
+
case BITS_ANY_SET:
- debug << "$bitsAnySet:";
- break;
+ return "$bitsAnySet";
+
case BITS_ANY_CLEAR:
- debug << "$bitsAnyClear:";
- break;
+ return "$bitsAnyClear";
+
default:
MONGO_UNREACHABLE;
}
+}
+
+void BitTestMatchExpression::debugString(StringBuilder& debug, int indentationLevel) const {
+ _debugAddSpace(debug, indentationLevel);
- debug << " [";
+ debug << path() << " " << name() << ": [";
for (size_t i = 0; i < _bitPositions.size(); i++) {
debug << _bitPositions[i];
if (i != _bitPositions.size() - 1) {
diff --git a/src/mongo/db/matcher/expression_leaf.h b/src/mongo/db/matcher/expression_leaf.h
index 248e68f1f44..f2c3a5f13a3 100644
--- a/src/mongo/db/matcher/expression_leaf.h
+++ b/src/mongo/db/matcher/expression_leaf.h
@@ -693,6 +693,8 @@ public:
return _bitMask;
}
+ std::string name() const;
+
private:
ExpressionOptimizerFunc getOptimizer() const final {
return [](std::unique_ptr<MatchExpression> expression) { return expression; };
diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp
index 8839b537f4c..06016d8c3a4 100644
--- a/src/mongo/db/matcher/expression_parser.cpp
+++ b/src/mongo/db/matcher/expression_parser.cpp
@@ -69,6 +69,7 @@
#include "mongo/db/namespace_string.h"
#include "mongo/db/query/dbref.h"
#include "mongo/db/query/query_knobs_gen.h"
+#include "mongo/db/stats/counters.h"
#include "mongo/util/str.h"
#include "mongo/util/string_map.h"
@@ -173,6 +174,7 @@ StatusWithMatchExpression parseRegexElement(StringData name,
if (e.type() != BSONType::RegEx)
return {Status(ErrorCodes::BadValue, "not a regex")};
+ expCtx->incrementMatchExprCounter("$regex");
return {std::make_unique<RegexMatchExpression>(
name,
e.regex(),
@@ -319,6 +321,7 @@ StatusWithMatchExpression parse(const BSONObj& obj,
if (auto&& expr = parsedExpression.getValue())
root->add(std::move(expr));
+ expCtx->incrementMatchExprCounter(e.fieldNameStringData());
continue;
}
@@ -345,7 +348,6 @@ StatusWithMatchExpression parse(const BSONObj& obj,
auto result = parseRegexElement(e.fieldNameStringData(), e, expCtx);
if (!result.isOK())
return result;
-
addExpressionToRoot(expCtx, root.get(), std::move(result.getValue()));
continue;
}
@@ -361,7 +363,7 @@ StatusWithMatchExpression parse(const BSONObj& obj,
allowedFeatures);
if (!eq.isOK())
return eq;
-
+ expCtx->incrementMatchExprCounter("$eq");
addExpressionToRoot(expCtx, root.get(), std::move(eq.getValue()));
}
@@ -423,7 +425,9 @@ StatusWithMatchExpression parseSampleRate(StringData name,
// DeMorgan's law here you will be suprised that $sampleRate will accept NaN as a valid
// argument.
return {Status(ErrorCodes::BadValue, "numeric argument to $sampleRate must be in [0, 1]")};
- } else if (x == kRandomMinValue) {
+ }
+
+ if (x == kRandomMinValue) {
return std::make_unique<ExprMatchExpression>(
ExpressionConstant::create(expCtx.get(), Value(false)),
expCtx,
@@ -1202,6 +1206,7 @@ StatusWithMatchExpression parseGeo(StringData name,
return status;
}
expCtx->sbeCompatible = false;
+ expCtx->incrementMatchExprCounter(section.firstElementFieldName());
return {std::make_unique<GeoNearMatchExpression>(name, nq.release(), section)};
}
}
@@ -2017,6 +2022,7 @@ Status parseSub(StringData name,
if (!s.isOK())
return s.getStatus();
+ expCtx->incrementMatchExprCounter(deep.fieldNameStringData());
if (s.getValue()) {
addExpressionToRoot(expCtx, root, std::move(s.getValue()));
}
@@ -2179,6 +2185,33 @@ retrievePathlessParser(StringData name) {
}
return func->second;
}
+
+MONGO_INITIALIZER_WITH_PREREQUISITES(MatchExpressionCounters,
+ ("PathlessOperatorMap", "MatchExpressionParser"))
+(InitializerContext* context) {
+ static const std::set<std::string> exceptionsSet{"within", // deprecated
+ "geoNear", // aggregation stage
+ "db", // $-prefixed field names
+ "id",
+ "ref",
+ "options"};
+
+ for (auto&& [name, keyword] : *queryOperatorMap) {
+ if (name[0] == '_' || exceptionsSet.count(name) > 0) {
+ continue;
+ }
+ operatorCountersMatchExpressions.addMatchExprCounter("$" + name);
+ }
+ for (auto&& [name, fn] : *pathlessOperatorMap) {
+ if (name[0] == '_' || exceptionsSet.count(name) > 0) {
+ continue;
+ }
+ operatorCountersMatchExpressions.addMatchExprCounter("$" + name);
+ }
+ operatorCountersMatchExpressions.addMatchExprCounter("$not");
+}
+
+
} // namespace
boost::optional<PathAcceptingKeyword> MatchExpressionParser::parsePathAcceptingKeyword(
diff --git a/src/mongo/db/pipeline/expression_context.cpp b/src/mongo/db/pipeline/expression_context.cpp
index c4cb0f6e411..69698ab0289 100644
--- a/src/mongo/db/pipeline/expression_context.cpp
+++ b/src/mongo/db/pipeline/expression_context.cpp
@@ -36,6 +36,7 @@
#include "mongo/db/pipeline/process_interface/stub_mongo_process_interface.h"
#include "mongo/db/query/collation/collation_spec.h"
#include "mongo/db/query/collation/collator_factory_interface.h"
+#include "mongo/db/stats/counters.h"
#include "mongo/util/intrusive_counter.h"
namespace mongo {
@@ -223,4 +224,24 @@ intrusive_ptr<ExpressionContext> ExpressionContext::copyWith(
return expCtx;
}
+void ExpressionContext::startExpressionCounters() {
+ if (!_expressionCounters) {
+ _expressionCounters = boost::make_optional<ExpressionCounters>({});
+ }
+}
+
+void ExpressionContext::incrementMatchExprCounter(StringData name) {
+ if (_expressionCounters) {
+ ++_expressionCounters.get().matchExprCountersMap[name];
+ }
+}
+
+void ExpressionContext::stopExpressionCounters() {
+ if (_expressionCounters) {
+ operatorCountersMatchExpressions.mergeCounters(
+ _expressionCounters.get().matchExprCountersMap);
+ }
+ _expressionCounters = boost::none;
+}
+
} // namespace mongo
diff --git a/src/mongo/db/pipeline/expression_context.h b/src/mongo/db/pipeline/expression_context.h
index b40028dc30a..398a162b283 100644
--- a/src/mongo/db/pipeline/expression_context.h
+++ b/src/mongo/db/pipeline/expression_context.h
@@ -58,6 +58,15 @@ namespace mongo {
class AggregateCommandRequest;
+/**
+ * The structure ExpressionCounters encapsulates counters for match, aggregate, and other
+ * expression types as seen in the end-user queries.
+ */
+struct ExpressionCounters {
+ StringMap<uint64_t> aggExprCountersMap;
+ StringMap<uint64_t> matchExprCountersMap;
+};
+
class ExpressionContext : public RefCountable {
public:
static constexpr size_t kMaxSubPipelineViewDepth = 20;
@@ -316,6 +325,22 @@ public:
return JsExecution::get(opCtx, scopeObj, ns.db(), loadStoredProcedures, jsHeapLimitMB);
}
+ /**
+ * Create optional internal expression counters and start counting.
+ */
+ void startExpressionCounters();
+
+ /**
+ * Increment the counter for the match expression with a given name.
+ */
+ void incrementMatchExprCounter(StringData name);
+
+ /**
+ * Merge expression counters from the current expression context into the global maps
+ * and stop counting.
+ */
+ void stopExpressionCounters();
+
// The explain verbosity requested by the user, or boost::none if no explain was requested.
boost::optional<ExplainOptions::Verbosity> explain;
@@ -413,6 +438,9 @@ protected:
StringMap<ResolvedNamespace> _resolvedNamespaces;
int _interruptCounter = kInterruptCheckPeriod;
+
+private:
+ boost::optional<ExpressionCounters> _expressionCounters = boost::none;
};
} // namespace mongo
diff --git a/src/mongo/db/query/canonical_query.cpp b/src/mongo/db/query/canonical_query.cpp
index de2e39895ff..87efc4cc259 100644
--- a/src/mongo/db/query/canonical_query.cpp
+++ b/src/mongo/db/query/canonical_query.cpp
@@ -126,6 +126,11 @@ StatusWith<std::unique_ptr<CanonicalQuery>> CanonicalQuery::canonicalize(
if (!statusWithMatcher.isOK()) {
return statusWithMatcher.getStatus();
}
+
+ // Stop counting match expressions after they have been parsed to exclude expressions created
+ // during optimization and other processing steps.
+ newExpCtx->stopExpressionCounters();
+
std::unique_ptr<MatchExpression> me = std::move(statusWithMatcher.getValue());
Status initStatus =
diff --git a/src/mongo/db/stats/counters.cpp b/src/mongo/db/stats/counters.cpp
index 1cf3e4f4f72..2f9b070b888 100644
--- a/src/mongo/db/stats/counters.cpp
+++ b/src/mongo/db/stats/counters.cpp
@@ -314,4 +314,5 @@ AuthCounter authCounter;
AggStageCounters aggStageCounters;
DotsAndDollarsFieldsCounters dotsAndDollarsFieldsCounters;
OperatorCountersExpressions operatorCountersExpressions;
+OperatorCountersMatchExpressions operatorCountersMatchExpressions;
} // namespace mongo
diff --git a/src/mongo/db/stats/counters.h b/src/mongo/db/stats/counters.h
index 397951bdd7b..18e39b1366a 100644
--- a/src/mongo/db/stats/counters.h
+++ b/src/mongo/db/stats/counters.h
@@ -363,4 +363,37 @@ private:
};
extern OperatorCountersExpressions operatorCountersExpressions;
+
+/**
+ * Global counters for match expressions.
+ */
+class OperatorCountersMatchExpressions {
+private:
+ struct MatchExprCounter {
+ MatchExprCounter(StringData name) : metric("operatorCounters.match." + name, &counter) {}
+
+ Counter64 counter;
+ ServerStatusMetricField<Counter64> metric;
+ };
+
+public:
+ void addMatchExprCounter(StringData name) {
+ operatorCountersMatchExprMap[name] = std::make_unique<MatchExprCounter>(name);
+ }
+
+ void mergeCounters(StringMap<uint64_t>& toMerge) {
+ for (auto&& [name, cnt] : toMerge) {
+ if (auto it = operatorCountersMatchExprMap.find(name);
+ it != operatorCountersMatchExprMap.end()) {
+ it->second->counter.increment(cnt);
+ }
+ }
+ }
+
+private:
+ // Map of match expressions to the number of occurrences in queries.
+ StringMap<std::unique_ptr<MatchExprCounter>> operatorCountersMatchExprMap = {};
+};
+
+extern OperatorCountersMatchExpressions operatorCountersMatchExpressions;
} // namespace mongo