summaryrefslogtreecommitdiff
path: root/src/mongo/db/query
diff options
context:
space:
mode:
authorArun Banala <arun.banala@mongodb.com>2019-07-12 13:15:30 +0100
committerArun Banala <arun.banala@mongodb.com>2019-08-26 12:00:59 +0100
commit5eeba7b2c9dda32a37c2c16ca14edd9a9099a996 (patch)
tree5aba76502039c601b5ed33ddb936aa3369b87001 /src/mongo/db/query
parent7897a0a4ef9ee0e6beb9d384f5ea5a6e7c187fce (diff)
downloadmongo-5eeba7b2c9dda32a37c2c16ca14edd9a9099a996.tar.gz
SERVER-40382 Add a serverStatus metric to report plan cache memory consumption
(cherry picked from commit 5105fa2377d3e86b2011691d5acbd8c531113929)
Diffstat (limited to 'src/mongo/db/query')
-rw-r--r--src/mongo/db/query/SConscript1
-rw-r--r--src/mongo/db/query/index_entry.h30
-rw-r--r--src/mongo/db/query/plan_cache.cpp169
-rw-r--r--src/mongo/db/query/plan_cache.h103
-rw-r--r--src/mongo/db/query/plan_cache_test.cpp188
-rw-r--r--src/mongo/db/query/plan_ranker.h13
6 files changed, 421 insertions, 83 deletions
diff --git a/src/mongo/db/query/SConscript b/src/mongo/db/query/SConscript
index a2183a6f2fb..774678220d1 100644
--- a/src/mongo/db/query/SConscript
+++ b/src/mongo/db/query/SConscript
@@ -42,6 +42,7 @@ env.Library(
LIBDEPS=[
"$BUILD_DIR/mongo/base",
"$BUILD_DIR/mongo/db/bson/dotted_path_support",
+ '$BUILD_DIR/mongo/db/commands/server_status_core',
"$BUILD_DIR/mongo/db/index/expression_params",
"$BUILD_DIR/mongo/db/index/key_generator",
"$BUILD_DIR/mongo/db/index_names",
diff --git a/src/mongo/db/query/index_entry.h b/src/mongo/db/query/index_entry.h
index ede83b8e700..efe9179bc1d 100644
--- a/src/mongo/db/query/index_entry.h
+++ b/src/mongo/db/query/index_entry.h
@@ -37,6 +37,7 @@
#include "mongo/db/index/multikey_paths.h"
#include "mongo/db/index_names.h"
#include "mongo/db/jsobj.h"
+#include "mongo/util/container_size_helper.h"
#include "mongo/util/str.h"
namespace mongo {
@@ -109,6 +110,9 @@ struct CoreIndexInfo {
return "(" + catalogName + ", " + disambiguator + ")";
}
+ uint64_t estimateObjectSizeInBytes() const {
+ return catalogName.capacity() + disambiguator.capacity() + sizeof(*this);
+ }
// The name of the index in the catalog.
std::string catalogName;
@@ -195,6 +199,32 @@ struct IndexEntry : CoreIndexInfo {
std::string toString() const;
+ uint64_t estimateObjectSizeInBytes() const {
+
+ return // For each element in 'multikeyPaths' add the 'length of the vector * size of the
+ // vector element'.
+ container_size_helper::estimateObjectSizeInBytes(
+ multikeyPaths,
+ [](const auto& keyPath) {
+ // Calculate the size of each std::set in 'multiKeyPaths'.
+ return container_size_helper::estimateObjectSizeInBytes(keyPath);
+ },
+ true) +
+ container_size_helper::estimateObjectSizeInBytes(
+ multikeyPathSet,
+ [](const auto& fieldRef) { return fieldRef.estimateObjectSizeInBytes(); },
+ false) +
+ // Subtract static size of 'identifier' since it is already included in
+ // 'sizeof(*this)'.
+ (identifier.estimateObjectSizeInBytes() - sizeof(identifier)) +
+ // Add the runtime BSONObj size of 'keyPattern'.
+ keyPattern.objsize() +
+ // The BSON size of the 'infoObj' is purposefully excluded since its ownership is shared
+ // with the index catalog.
+ // Add size of the object.
+ sizeof(*this);
+ }
+
bool multikey;
// If non-empty, 'multikeyPaths' is a vector with size equal to the number of elements in the
diff --git a/src/mongo/db/query/plan_cache.cpp b/src/mongo/db/query/plan_cache.cpp
index 4defd1cdd20..ba451f112a6 100644
--- a/src/mongo/db/query/plan_cache.cpp
+++ b/src/mongo/db/query/plan_cache.cpp
@@ -42,6 +42,7 @@
#include "mongo/base/owned_pointer_vector.h"
#include "mongo/base/string_data_comparator_interface.h"
+#include "mongo/db/commands/server_status_metric.h"
#include "mongo/db/matcher/expression_array.h"
#include "mongo/db/matcher/expression_geo.h"
#include "mongo/db/query/canonical_query_encoder.h"
@@ -58,6 +59,9 @@
namespace mongo {
namespace {
+ServerStatusMetricField<Counter64> totalPlanCacheSizeEstimateBytesMetric(
+ "query.planCacheTotalSizeEstimateBytes", &PlanCacheEntry::planCacheTotalSizeEstimateBytes);
+
// Delimiters for cache key encoding.
const char kEncodeDiscriminatorsBegin = '<';
const char kEncodeDiscriminatorsEnd = '>';
@@ -192,59 +196,125 @@ CachedSolution::~CachedSolution() {
// PlanCacheEntry
//
-PlanCacheEntry::PlanCacheEntry(const std::vector<QuerySolution*>& solutions,
- PlanRankingDecision* why,
- uint32_t queryHash,
- uint32_t planCacheKey)
- : plannerData(solutions.size()),
- queryHash(queryHash),
- planCacheKey(planCacheKey),
- decision(why) {
- invariant(why);
+std::unique_ptr<PlanCacheEntry> PlanCacheEntry::create(
+ const std::vector<QuerySolution*>& solutions,
+ std::unique_ptr<const PlanRankingDecision> decision,
+ const CanonicalQuery& query,
+ uint32_t queryHash,
+ uint32_t planCacheKey,
+ Date_t timeOfCreation,
+ bool isActive,
+ size_t works) {
+ invariant(decision);
// The caller of this constructor is responsible for ensuring
// that the QuerySolution 's' has valid cacheData. If there's no
// data to cache you shouldn't be trying to construct a PlanCacheEntry.
// Copy the solution's cache data into the plan cache entry.
+ std::vector<std::unique_ptr<const SolutionCacheData>> solutionCacheData(solutions.size());
for (size_t i = 0; i < solutions.size(); ++i) {
invariant(solutions[i]->cacheData.get());
- plannerData[i] = solutions[i]->cacheData->clone();
+ solutionCacheData[i] =
+ std::unique_ptr<const SolutionCacheData>(solutions[i]->cacheData->clone());
}
+
+ // Strip projections on $-prefixed fields, as these are added by internal callers of the
+ // system and are not considered part of the user projection.
+ const QueryRequest& qr = query.getQueryRequest();
+ BSONObjBuilder projBuilder;
+ for (auto elem : qr.getProj()) {
+ if (elem.fieldName()[0] == '$') {
+ continue;
+ }
+ projBuilder.append(elem);
+ }
+
+ return std::unique_ptr<PlanCacheEntry>(new PlanCacheEntry(
+ std::move(solutionCacheData),
+ qr.getFilter(),
+ qr.getSort(),
+ projBuilder.obj(),
+ query.getCollator() ? query.getCollator()->getSpec().toBSON() : BSONObj(),
+ timeOfCreation,
+ queryHash,
+ planCacheKey,
+ std::move(decision),
+ {},
+ isActive,
+ works));
+}
+
+PlanCacheEntry::PlanCacheEntry(std::vector<std::unique_ptr<const SolutionCacheData>> plannerData,
+ const BSONObj& query,
+ const BSONObj& sort,
+ const BSONObj& projection,
+ const BSONObj& collation,
+ const Date_t timeOfCreation,
+ const uint32_t queryHash,
+ const uint32_t planCacheKey,
+ std::unique_ptr<const PlanRankingDecision> decision,
+ std::vector<double> feedback,
+ const bool isActive,
+ const size_t works)
+ : plannerData(std::move(plannerData)),
+ query(query),
+ sort(sort),
+ projection(projection),
+ collation(collation),
+ timeOfCreation(timeOfCreation),
+ queryHash(queryHash),
+ planCacheKey(planCacheKey),
+ decision(std::move(decision)),
+ feedback(std::move(feedback)),
+ isActive(isActive),
+ works(works),
+ _entireObjectSize(_estimateObjectSizeInBytes()) {
+ // Account for the object in the global metric for estimating the server's total plan cache
+ // memory consumption.
+ planCacheTotalSizeEstimateBytes.increment(_entireObjectSize);
}
PlanCacheEntry::~PlanCacheEntry() {
- for (size_t i = 0; i < plannerData.size(); ++i) {
- delete plannerData[i];
- }
+ planCacheTotalSizeEstimateBytes.decrement(_entireObjectSize);
}
-PlanCacheEntry* PlanCacheEntry::clone() const {
- std::vector<std::unique_ptr<QuerySolution>> solutions;
+std::unique_ptr<PlanCacheEntry> PlanCacheEntry::clone() const {
+ std::vector<std::unique_ptr<const SolutionCacheData>> solutionCacheData(plannerData.size());
for (size_t i = 0; i < plannerData.size(); ++i) {
- auto qs = stdx::make_unique<QuerySolution>();
- qs->cacheData.reset(plannerData[i]->clone());
- solutions.push_back(std::move(qs));
- }
- PlanCacheEntry* entry =
- new PlanCacheEntry(transitional_tools_do_not_use::unspool_vector(solutions),
- decision->clone(),
- queryHash,
- planCacheKey);
-
- // Copy query shape.
- entry->query = query.getOwned();
- entry->sort = sort.getOwned();
- entry->projection = projection.getOwned();
- entry->collation = collation.getOwned();
- entry->timeOfCreation = timeOfCreation;
- entry->isActive = isActive;
- entry->works = works;
-
- // Copy performance stats.
- entry->feedback = feedback;
-
- return entry;
+ invariant(plannerData[i]);
+ solutionCacheData[i] = std::unique_ptr<const SolutionCacheData>(plannerData[i]->clone());
+ }
+
+ auto decisionPtr = std::unique_ptr<PlanRankingDecision>(decision->clone());
+ return std::unique_ptr<PlanCacheEntry>(new PlanCacheEntry(std::move(solutionCacheData),
+ query,
+ sort,
+ projection,
+ collation,
+ timeOfCreation,
+ queryHash,
+ planCacheKey,
+ std::move(decisionPtr),
+ feedback,
+ isActive,
+ works));
+}
+
+uint64_t PlanCacheEntry::_estimateObjectSizeInBytes() const {
+ return // Add the size of each entry in 'plannerData' vector.
+ container_size_helper::estimateObjectSizeInBytes(
+ plannerData,
+ [](const auto& cacheData) { return cacheData->estimateObjectSizeInBytes(); },
+ true) +
+ // Add size of each entry in '_feedback' vector.
+ container_size_helper::estimateObjectSizeInBytes(feedback) +
+ // Add the entire size of 'decision' object.
+ (decision ? decision->estimateObjectSizeInBytes() : 0) +
+ // Add the size of all the owned BSON objects.
+ query.objsize() + sort.objsize() + projection.objsize() + collation.objsize() +
+ // Add size of the object.
+ sizeof(*this);
}
std::string PlanCacheEntry::toString() const {
@@ -518,27 +588,8 @@ Status PlanCache::set(const CanonicalQuery& query,
isNewEntryActive = newState.shouldBeActive;
}
- auto newEntry = std::make_unique<PlanCacheEntry>(solns, why.release(), queryHash, planCacheKey);
- const QueryRequest& qr = query.getQueryRequest();
- newEntry->query = qr.getFilter().getOwned();
- newEntry->sort = qr.getSort().getOwned();
- newEntry->isActive = isNewEntryActive;
- newEntry->works = newWorks;
- if (query.getCollator()) {
- newEntry->collation = query.getCollator()->getSpec().toBSON();
- }
- newEntry->timeOfCreation = now;
-
- // Strip projections on $-prefixed fields, as these are added by internal callers of the query
- // system and are not considered part of the user projection.
- BSONObjBuilder projBuilder;
- for (auto elem : qr.getProj()) {
- if (elem.fieldName()[0] == '$') {
- continue;
- }
- projBuilder.append(elem);
- }
- newEntry->projection = projBuilder.obj();
+ auto newEntry(PlanCacheEntry::create(
+ solns, std::move(why), query, queryHash, planCacheKey, now, isNewEntryActive, newWorks));
std::unique_ptr<PlanCacheEntry> evictedEntry = _cache.add(key, newEntry.release());
diff --git a/src/mongo/db/query/plan_cache.h b/src/mongo/db/query/plan_cache.h
index 3c904ec3794..0b8d6216678 100644
--- a/src/mongo/db/query/plan_cache.h
+++ b/src/mongo/db/query/plan_cache.h
@@ -32,6 +32,7 @@
#include <boost/optional/optional.hpp>
#include <set>
+#include "mongo/base/counter.h"
#include "mongo/db/exec/plan_stats.h"
#include "mongo/db/query/canonical_query.h"
#include "mongo/db/query/index_tag.h"
@@ -40,6 +41,7 @@
#include "mongo/db/query/query_planner_params.h"
#include "mongo/platform/atomic_word.h"
#include "mongo/stdx/mutex.h"
+#include "mongo/util/container_size_helper.h"
namespace mongo {
@@ -139,6 +141,15 @@ struct PlanCacheIndexTree {
* or satisfy the first field in the index.
*/
struct OrPushdown {
+ uint64_t estimateObjectSizeInBytes() const {
+ return // Add size of each element in 'route' vector.
+ container_size_helper::estimateObjectSizeInBytes(route) +
+ // Subtract static size of 'identifier' since it is already included in
+ // 'sizeof(*this)'.
+ (indexEntryId.estimateObjectSizeInBytes() - sizeof(indexEntryId)) +
+ // Add size of the object.
+ sizeof(*this);
+ }
IndexEntry::Identifier indexEntryId;
size_t position;
bool canCombineBounds;
@@ -170,6 +181,22 @@ struct PlanCacheIndexTree {
*/
std::string toString(int indents = 0) const;
+ uint64_t estimateObjectSizeInBytes() const {
+ return // Recursively add size of each element in 'children' vector.
+ container_size_helper::estimateObjectSizeInBytes(
+ children,
+ [](const auto& child) { return child->estimateObjectSizeInBytes(); },
+ true) +
+ // Add size of each element in 'orPushdowns' vector.
+ container_size_helper::estimateObjectSizeInBytes(
+ orPushdowns,
+ [](const auto& orPushdown) { return orPushdown.estimateObjectSizeInBytes(); },
+ false) +
+ // Add size of 'entry' if present.
+ (entry ? entry->estimateObjectSizeInBytes() : 0) +
+ // Add size of the object.
+ sizeof(*this);
+ }
// Children owned here.
std::vector<PlanCacheIndexTree*> children;
@@ -205,6 +232,10 @@ struct SolutionCacheData {
// For debugging.
std::string toString() const;
+ uint64_t estimateObjectSizeInBytes() const {
+ return (tree ? tree->estimateObjectSizeInBytes() : 0) + sizeof(*this);
+ }
+
// Owned here. If 'wholeIXSoln' is false, then 'tree'
// can be used to tag an isomorphic match expression. If 'wholeIXSoln'
// is true, then 'tree' is used to store the relevant IndexEntry.
@@ -275,27 +306,28 @@ public:
* Also used by the plan cache commands to display plan cache state.
*/
class PlanCacheEntry {
-private:
- PlanCacheEntry(const PlanCacheEntry&) = delete;
- PlanCacheEntry& operator=(const PlanCacheEntry&) = delete;
public:
/**
* Create a new PlanCacheEntry.
* Grabs any planner-specific data required from the solutions.
- * Takes ownership of the PlanRankingDecision that placed the plan in the cache.
*/
- PlanCacheEntry(const std::vector<QuerySolution*>& solutions,
- PlanRankingDecision* why,
- uint32_t queryHash,
- uint32_t planCacheKey);
+ static std::unique_ptr<PlanCacheEntry> create(
+ const std::vector<QuerySolution*>& solutions,
+ std::unique_ptr<const PlanRankingDecision> decision,
+ const CanonicalQuery& query,
+ uint32_t queryHash,
+ uint32_t planCacheKey,
+ Date_t timeOfCreation,
+ bool isActive,
+ size_t works);
~PlanCacheEntry();
/**
* Make a deep copy.
*/
- PlanCacheEntry* clone() const;
+ std::unique_ptr<PlanCacheEntry> clone() const;
// For debugging.
std::string toString() const;
@@ -307,33 +339,32 @@ public:
// Data provided to the planner to allow it to recreate the solutions this entry
// represents. Each SolutionCacheData is fully owned here, so in order to return
// it from the cache a deep copy is made and returned inside CachedSolution.
- std::vector<SolutionCacheData*> plannerData;
+ const std::vector<std::unique_ptr<const SolutionCacheData>> plannerData;
// TODO: Do we really want to just hold a copy of the CanonicalQuery? For now we just
// extract the data we need.
//
// Used by the plan cache commands to display an example query
// of the appropriate shape.
- BSONObj query;
- BSONObj sort;
- BSONObj projection;
- BSONObj collation;
- Date_t timeOfCreation;
+ const BSONObj query;
+ const BSONObj sort;
+ const BSONObj projection;
+ const BSONObj collation;
+ const Date_t timeOfCreation;
// Hash of the PlanCacheKey. Intended as an identifier for the query shape in logs and other
// diagnostic output.
- uint32_t queryHash;
+ const uint32_t queryHash;
// Hash of the "stable" PlanCacheKey, which is the same regardless of what indexes are around.
- uint32_t planCacheKey;
+ const uint32_t planCacheKey;
//
// Performance stats
//
- // Information that went into picking the winning plan and also why
- // the other plans lost.
- std::unique_ptr<PlanRankingDecision> decision;
+ // Information that went into picking the winning plan and also why the other plans lost.
+ const std::unique_ptr<const PlanRankingDecision> decision;
// Scores from uses of this cache entry.
std::vector<double> feedback;
@@ -347,6 +378,38 @@ public:
// trigger a replan. Running a query of the same shape while this cache entry is inactive may
// cause this value to be increased.
size_t works = 0;
+
+ /**
+ * Tracks the approximate cumulative size of the plan cache entries across all the collections.
+ */
+ inline static Counter64 planCacheTotalSizeEstimateBytes;
+
+private:
+ /**
+ * All arguments constructor.
+ */
+ PlanCacheEntry(std::vector<std::unique_ptr<const SolutionCacheData>> plannerData,
+ const BSONObj& query,
+ const BSONObj& sort,
+ const BSONObj& projection,
+ const BSONObj& collation,
+ Date_t timeOfCreation,
+ uint32_t queryHash,
+ uint32_t planCacheKey,
+ std::unique_ptr<const PlanRankingDecision> decision,
+ std::vector<double> feedback,
+ bool isActive,
+ size_t works);
+
+ // Ensure that PlanCacheEntry is non-copyable.
+ PlanCacheEntry(const PlanCacheEntry&) = delete;
+ PlanCacheEntry& operator=(const PlanCacheEntry&) = delete;
+
+ uint64_t _estimateObjectSizeInBytes() const;
+
+ // The total runtime size of the current object in bytes. This is the deep size, obtained by
+ // recursively following references to all owned objects.
+ const uint64_t _entireObjectSize;
};
/**
diff --git a/src/mongo/db/query/plan_cache_test.cpp b/src/mongo/db/query/plan_cache_test.cpp
index 8507ab4707f..e0e64f898f6 100644
--- a/src/mongo/db/query/plan_cache_test.cpp
+++ b/src/mongo/db/query/plan_cache_test.cpp
@@ -87,8 +87,8 @@ unique_ptr<CanonicalQuery> canonicalize(const BSONObj& queryObj) {
return std::move(statusWithCQ.getValue());
}
-unique_ptr<CanonicalQuery> canonicalize(const char* queryStr) {
- BSONObj queryObj = fromjson(queryStr);
+unique_ptr<CanonicalQuery> canonicalize(StringData queryStr) {
+ BSONObj queryObj = fromjson(queryStr.toString());
return canonicalize(queryObj);
}
@@ -1132,8 +1132,9 @@ protected:
uint32_t queryHash = canonical_query_encoder::computeHash(ck.stringData());
uint32_t planCacheKey = queryHash;
- PlanCacheEntry entry(solutions, createDecision(1U).release(), queryHash, planCacheKey);
- CachedSolution cachedSoln(ck, entry);
+ auto entry = PlanCacheEntry::create(
+ solutions, createDecision(1U), *scopedCq, queryHash, planCacheKey, Date_t(), false, 0);
+ CachedSolution cachedSoln(ck, *entry);
auto statusWithQs = QueryPlanner::planFromCache(*scopedCq, params, cachedSoln);
ASSERT_OK(statusWithQs.getStatus());
@@ -2129,4 +2130,183 @@ TEST(PlanCacheTest, PlanCacheKeyCollision) {
ASSERT_NE(keyAWithIndex.getUnstablePart(), keyBWithIndex.getUnstablePart());
}
+TEST(PlanCacheTest, PlanCacheSizeWithCRUDOperations) {
+ PlanCache planCache;
+ unique_ptr<CanonicalQuery> cq(canonicalize("{a: 1, b: 1}"));
+ auto qs = getQuerySolutionForCaching();
+ std::vector<QuerySolution*> solns = {qs.get()};
+ long long previousSize, originalSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+
+ // Verify that the plan cache size increases after adding new entry to cache.
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache.set(*cq, solns, createDecision(1U), Date_t{}));
+ ASSERT_GT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+
+ // Verify that trying to set the same entry won't change the plan cache size.
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache.set(*cq, solns, createDecision(1U), Date_t{}));
+ ASSERT_EQ(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+
+ // Verify that the plan cache size increases after updating the same entry with more solutions.
+ solns.push_back(qs.get());
+ ASSERT_OK(planCache.set(*cq, solns, createDecision(2U), Date_t{}));
+ ASSERT_GT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+
+ // Verify that the plan cache size decreases after updating the same entry with fewer solutions.
+ solns.erase(solns.end() - 1);
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache.set(*cq, solns, createDecision(1U), Date_t{}));
+ ASSERT_LT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+ ASSERT_GT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), originalSize);
+
+ // Verify that adding multiple entries will increasing the cache size.
+ long long sizeWithOneEntry = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ std::string queryString = "{a: 1, c: 1}";
+ for (int i = 0; i < 5; ++i) {
+ // Update the field name in the query string so that plan cache creates a new entry.
+ queryString[1] = 'b' + i;
+ unique_ptr<CanonicalQuery> query(canonicalize(queryString));
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache.set(*query, solns, createDecision(1U), Date_t{}));
+ ASSERT_GT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+ }
+
+ // Verify that removing multiple entries will decreasing the cache size.
+ for (int i = 0; i < 5; ++i) {
+ // Update the field name in the query to match the previously created plan cache entry key.
+ queryString[1] = 'b' + i;
+ unique_ptr<CanonicalQuery> query(canonicalize(queryString));
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache.remove(*query));
+ ASSERT_LT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+ }
+ // Verify that size is reset to the size when there is only entry.
+ ASSERT_EQ(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), sizeWithOneEntry);
+
+ // Verify that trying to remove a non-existing key won't change the plan cache size.
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ unique_ptr<CanonicalQuery> newQuery(canonicalize("{a: 1}"));
+ ASSERT_NOT_OK(planCache.remove(*newQuery));
+ ASSERT_EQ(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+
+ // Verify that the plan cache size goes back to original size when the entry is removed.
+ ASSERT_OK(planCache.remove(*cq));
+ ASSERT_EQ(planCache.size(), 0U);
+ ASSERT_EQ(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), originalSize);
+}
+
+TEST(PlanCacheTest, PlanCacheSizeWithEviction) {
+ const size_t kCacheSize = 5;
+ PlanCache planCache(kCacheSize);
+ unique_ptr<CanonicalQuery> cq(canonicalize("{a: 1, b: 1}"));
+ auto qs = getQuerySolutionForCaching();
+ std::vector<QuerySolution*> solns = {qs.get(), qs.get()};
+ long long originalSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ long long previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+
+ // Add entries until plan cache is full and verify that the size keeps increasing.
+ std::string queryString = "{a: 1, c: 1}";
+ for (size_t i = 0; i < kCacheSize; ++i) {
+ // Update the field name in the query string so that plan cache creates a new entry.
+ queryString[1]++;
+ unique_ptr<CanonicalQuery> query(canonicalize(queryString));
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache.set(*query, solns, createDecision(2U), Date_t{}));
+ ASSERT_GT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+ }
+
+ // Verify that adding entry of same size as evicted entry wouldn't change the plan cache size.
+ queryString = "{k: 1, c: 1}";
+ cq = unique_ptr<CanonicalQuery>(canonicalize(queryString));
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_EQ(planCache.size(), kCacheSize);
+ ASSERT_OK(planCache.set(*cq, solns, createDecision(2U), Date_t{}));
+ ASSERT_EQ(planCache.size(), kCacheSize);
+ ASSERT_EQ(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+
+ // Verify that adding entry with query bigger than the evicted entry's key should change the
+ // plan cache size.
+ queryString = "{k: 1, c: 1, extraField: 1}";
+ unique_ptr<CanonicalQuery> queryBiggerKey(canonicalize(queryString));
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache.set(*queryBiggerKey, solns, createDecision(2U), Date_t{}));
+ ASSERT_GT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+
+ // Verify that adding entry with query solutions larger than the evicted entry's query solutions
+ // should increase the plan cache size.
+ queryString = "{l: 1, c: 1}";
+ cq = unique_ptr<CanonicalQuery>(canonicalize(queryString));
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ solns.push_back(qs.get());
+ ASSERT_OK(planCache.set(*cq, solns, createDecision(3U), Date_t{}));
+ ASSERT_GT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+
+ // Verify that adding entry with query solutions smaller than the evicted entry's query
+ // solutions should decrease the plan cache size.
+ queryString = "{m: 1, c: 1}";
+ cq = unique_ptr<CanonicalQuery>(canonicalize(queryString));
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ solns = {qs.get()};
+ ASSERT_OK(planCache.set(*cq, solns, createDecision(1U), Date_t{}));
+ ASSERT_LT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+
+ // clear() should reset the size.
+ planCache.clear();
+ ASSERT_EQ(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), originalSize);
+}
+
+TEST(PlanCacheTest, PlanCacheSizeWithMultiplePlanCaches) {
+ PlanCache planCache1;
+ PlanCache planCache2;
+ unique_ptr<CanonicalQuery> cq(canonicalize("{a: 1, b: 1}"));
+ auto qs = getQuerySolutionForCaching();
+ std::vector<QuerySolution*> solns = {qs.get()};
+ long long previousSize, originalSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+
+ // Verify that adding entries to both plan caches will keep increasing the cache size.
+ std::string queryString = "{a: 1, c: 1}";
+ for (int i = 0; i < 5; ++i) {
+ // Update the field name in the query string so that plan cache creates a new entry.
+ queryString[1] = 'b' + i;
+ unique_ptr<CanonicalQuery> query(canonicalize(queryString));
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache1.set(*query, solns, createDecision(1U), Date_t{}));
+ ASSERT_GT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache2.set(*query, solns, createDecision(1U), Date_t{}));
+ ASSERT_GT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+ }
+
+ // Verify that removing entries from one plan caches will keep decreasing the cache size.
+ for (int i = 0; i < 5; ++i) {
+ // Update the field name in the query to match the previously created plan cache entry key.
+ queryString[1] = 'b' + i;
+ unique_ptr<CanonicalQuery> query(canonicalize(queryString));
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache1.remove(*query));
+ ASSERT_LT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+ }
+
+ // Verify for scoped PlanCache object.
+ long long sizeBeforeScopedPlanCache = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ {
+ PlanCache planCache;
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ ASSERT_OK(planCache.set(*cq, solns, createDecision(1U), Date_t{}));
+ ASSERT_GT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+ }
+
+ // Verify that size is reset to 'sizeBeforeScopedPlanCache' after the destructor of 'planCache'
+ // is called.
+ ASSERT_EQ(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), sizeBeforeScopedPlanCache);
+
+ // Clear 'planCache2' to remove all entries.
+ previousSize = PlanCacheEntry::planCacheTotalSizeEstimateBytes.get();
+ planCache2.clear();
+ ASSERT_LT(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), previousSize);
+
+ // Verify that size is reset to the original size after removing all entries.
+ ASSERT_EQ(PlanCacheEntry::planCacheTotalSizeEstimateBytes.get(), originalSize);
+}
} // namespace
diff --git a/src/mongo/db/query/plan_ranker.h b/src/mongo/db/query/plan_ranker.h
index 20c64e35b82..8018ffcef0f 100644
--- a/src/mongo/db/query/plan_ranker.h
+++ b/src/mongo/db/query/plan_ranker.h
@@ -38,6 +38,7 @@
#include "mongo/db/exec/plan_stats.h"
#include "mongo/db/exec/working_set.h"
#include "mongo/db/query/query_solution.h"
+#include "mongo/util/container_size_helper.h"
namespace mongo {
@@ -105,6 +106,18 @@ struct PlanRankingDecision {
return decision;
}
+ uint64_t estimateObjectSizeInBytes() const {
+ return // Add size of each element in 'stats' vector.
+ container_size_helper::estimateObjectSizeInBytes(
+ stats, [](const auto& stat) { return stat->estimateObjectSizeInBytes(); }, true) +
+ // Add size of each element in 'candidateOrder' vector.
+ container_size_helper::estimateObjectSizeInBytes(candidateOrder) +
+ // Add size of each element in 'scores' vector.
+ container_size_helper::estimateObjectSizeInBytes(scores) +
+ // Add size of the object.
+ sizeof(*this);
+ }
+
// Stats of all plans sorted in descending order by score.
// Owned by us.
std::vector<std::unique_ptr<PlanStageStats>> stats;