diff options
author | Arun Banala <arun.banala@mongodb.com> | 2019-07-12 13:15:30 +0100 |
---|---|---|
committer | Arun Banala <arun.banala@mongodb.com> | 2019-08-26 12:00:59 +0100 |
commit | 5eeba7b2c9dda32a37c2c16ca14edd9a9099a996 (patch) | |
tree | 5aba76502039c601b5ed33ddb936aa3369b87001 /src/mongo/db/query | |
parent | 7897a0a4ef9ee0e6beb9d384f5ea5a6e7c187fce (diff) | |
download | mongo-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/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/query/index_entry.h | 30 | ||||
-rw-r--r-- | src/mongo/db/query/plan_cache.cpp | 169 | ||||
-rw-r--r-- | src/mongo/db/query/plan_cache.h | 103 | ||||
-rw-r--r-- | src/mongo/db/query/plan_cache_test.cpp | 188 | ||||
-rw-r--r-- | src/mongo/db/query/plan_ranker.h | 13 |
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; |