cachelib/allocator/MM2Q-inl.h (345 lines of code) (raw):

/* * Copyright (c) Facebook, Inc. and its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace facebook { namespace cachelib { /* Container Interface Implementation */ template <typename T, MM2Q::Hook<T> T::*HookPtr> MM2Q::Container<T, HookPtr>::Container(const serialization::MM2QObject& object, PtrCompressor compressor) : lru_(*object.lrus(), compressor), tailTrackingEnabled_(*object.tailTrackingEnabled()), config_(*object.config()) { lruRefreshTime_ = config_.lruRefreshTime; nextReconfigureTime_ = config_.mmReconfigureIntervalSecs.count() == 0 ? std::numeric_limits<Time>::max() : static_cast<Time>(util::getCurrentTimeSec()) + config_.mmReconfigureIntervalSecs.count(); // We need to adjust list positions if the previous version does not have // tail lists (WarmTail & ColdTail), in order to potentially avoid cold roll if (object.lrus()->lists()->size() < LruType::NumTypes) { XDCHECK_EQ(false, tailTrackingEnabled_); XDCHECK_EQ(object.lrus()->lists()->size() + 2, LruType::NumTypes); lru_.insertEmptyListAt(LruType::WarmTail, compressor); lru_.insertEmptyListAt(LruType::ColdTail, compressor); } } template <typename T, MM2Q::Hook<T> T::*HookPtr> bool MM2Q::Container<T, HookPtr>::recordAccess(T& node, AccessMode mode) noexcept { if ((mode == AccessMode::kWrite && !config_.updateOnWrite) || (mode == AccessMode::kRead && !config_.updateOnRead)) { return false; } const auto curr = static_cast<Time>(util::getCurrentTimeSec()); // check if the node is still being memory managed if (node.isInMMContainer() && ((curr >= getUpdateTime(node) + lruRefreshTime_.load(std::memory_order_relaxed)))) { auto func = [&]() { reconfigureLocked(curr); if (!node.isInMMContainer()) { return false; } if (isHot(node)) { lru_.getList(LruType::Hot).moveToHead(node); ++numHotAccesses_; } else if (isCold(node)) { if (inTail(node)) { unmarkTail(node); lru_.getList(LruType::ColdTail).remove(node); ++numColdTailAccesses_; } else { lru_.getList(LruType::Cold).remove(node); } lru_.getList(LruType::Warm).linkAtHead(node); unmarkCold(node); ++numColdAccesses_; // only rebalance if config says so. recordAccess is called mostly on // latency sensitive cache get operations. if (config_.rebalanceOnRecordAccess) { rebalance(); } } else { if (inTail(node)) { unmarkTail(node); lru_.getList(LruType::WarmTail).remove(node); lru_.getList(LruType::Warm).linkAtHead(node); ++numWarmTailAccesses_; } else { lru_.getList(LruType::Warm).moveToHead(node); } ++numWarmAccesses_; } setUpdateTime(node, curr); return true; }; // if the tryLockUpdate optimization is on, and we were able to grab the // lock, execute the critical section and return true, else return false // // if the tryLockUpdate optimization is off, we always execute the critical // section and return true if (config_.tryLockUpdate) { if (auto lck = LockHolder{*lruMutex_, std::try_to_lock}) { return func(); } return false; } return lruMutex_->lock_combine(func); } return false; } template <typename T, MM2Q::Hook<T> T::*HookPtr> cachelib::EvictionAgeStat MM2Q::Container<T, HookPtr>::getEvictionAgeStat( uint64_t projectedLength) const noexcept { return lruMutex_->lock_combine([this, projectedLength]() { return getEvictionAgeStatLocked(projectedLength); }); } template <typename T, MM2Q::Hook<T> T::*HookPtr> cachelib::EvictionAgeStat MM2Q::Container<T, HookPtr>::getEvictionAgeStatLocked( uint64_t projectedLength) const noexcept { EvictionAgeStat stat; const auto curr = static_cast<Time>(util::getCurrentTimeSec()); stat.hotQueueStat.oldestElementAge = getOldestAgeLocked(LruType::Hot, curr); stat.hotQueueStat.size = lru_.getList(LruType::Hot).size(); // sum the tail and the main list for the ones that have tail. stat.warmQueueStat.oldestElementAge = getOldestAgeLocked(LruType::WarmTail, curr); stat.warmQueueStat.size = lru_.getList(LruType::Warm).size() + lru_.getList(LruType::WarmTail).size(); stat.coldQueueStat.oldestElementAge = getOldestAgeLocked(LruType::ColdTail, curr); stat.coldQueueStat.size = lru_.getList(LruType::ColdTail).size() + lru_.getList(LruType::Cold).size(); auto it = lru_.rbegin(LruType::WarmTail); for (size_t numSeen = 0; numSeen < projectedLength && it != lru_.rend(); ++numSeen, ++it) { } stat.projectedAge = (it != lru_.rend()) ? curr - getUpdateTime(*it) : stat.warmQueueStat.oldestElementAge; return stat; } template <typename T, MM2Q::Hook<T> T::*HookPtr> uint32_t MM2Q::Container<T, HookPtr>::getOldestAgeLocked( LruType lruType, Time currentTime) const noexcept { auto it = lru_.rbegin(lruType); return it != lru_.rend() ? currentTime - getUpdateTime(*it) : 0; } template <typename T, MM2Q::Hook<T> T::*HookPtr> typename MM2Q::LruType MM2Q::Container<T, HookPtr>::getLruType( const T& node) const noexcept { if (isHot(node)) { return LruType::Hot; } if (isCold(node)) { return inTail(node) ? LruType::ColdTail : LruType::Cold; } return inTail(node) ? LruType::WarmTail : LruType::Warm; } template <typename T, MM2Q::Hook<T> T::*HookPtr> void MM2Q::Container<T, HookPtr>::rebalance() noexcept { // shrink Warm (and WarmTail) if their total size is larger than expected size_t expectedSize = config_.getWarmSizePercent() * lru_.size() / 100; while (lru_.getList(LruType::Warm).size() + lru_.getList(LruType::WarmTail).size() > expectedSize) { auto popFrom = [&](LruType lruType) -> T* { auto& lru = lru_.getList(lruType); T* node = lru.getTail(); XDCHECK(node); lru.remove(*node); if (lruType == WarmTail || lruType == ColdTail) { unmarkTail(*node); } return node; }; // remove from warm tail if it is not empty. if empty, remove from warm. T* node = lru_.getList(LruType::WarmTail).size() > 0 ? popFrom(LruType::WarmTail) : popFrom(LruType::Warm); XDCHECK(isWarm(*node)); lru_.getList(LruType::Cold).linkAtHead(*node); markCold(*node); } // shrink Hot if its size is larger than expected expectedSize = config_.hotSizePercent * lru_.size() / 100; while (lru_.getList(LruType::Hot).size() > expectedSize) { auto node = lru_.getList(LruType::Hot).getTail(); XDCHECK(node); XDCHECK(isHot(*node)); lru_.getList(LruType::Hot).remove(*node); lru_.getList(LruType::Cold).linkAtHead(*node); unmarkHot(*node); markCold(*node); } // adjust tail sizes for Cold and Warm adjustTail(LruType::Cold); adjustTail(LruType::Warm); } template <typename T, MM2Q::Hook<T> T::*HookPtr> bool MM2Q::Container<T, HookPtr>::add(T& node) noexcept { const auto currTime = static_cast<Time>(util::getCurrentTimeSec()); return lruMutex_->lock_combine([this, &node, currTime]() { if (node.isInMMContainer()) { return false; } markHot(node); unmarkCold(node); unmarkTail(node); lru_.getList(LruType::Hot).linkAtHead(node); rebalance(); node.markInMMContainer(); setUpdateTime(node, currTime); return true; }); } template <typename T, MM2Q::Hook<T> T::*HookPtr> typename MM2Q::Container<T, HookPtr>::Iterator MM2Q::Container<T, HookPtr>::getEvictionIterator() const noexcept { // we cannot use combined critical sections with folly::DistributedMutex here // because the lock is held for the lifetime of the eviction iterator. In // other words, the abstraction of the iterator just does not lend itself well // to combinable critical sections as the user can hold the lock for an // arbitrary amount of time outside a lambda-friendly piece of code (eg. they // can return the iterator from functions, pass it to functions, etc) // // it would be theoretically possible to refactor this interface into // something like the following to allow combining // // mm2q.withEvictionIterator([&](auto iterator) { // // user code // }); // // at the time of writing it is unclear if the gains from combining are // reasonable justification for the codemod required to achieve combinability // as we don't expect this critical section to be the hotspot in user code. // This is however subject to change at some time in the future as and when // this assertion becomes false. LockHolder l(*lruMutex_); return Iterator{std::move(l), lru_.rbegin()}; } template <typename T, MM2Q::Hook<T> T::*HookPtr> void MM2Q::Container<T, HookPtr>::removeLocked(T& node, bool doRebalance) noexcept { LruType type = getLruType(node); lru_.getList(type).remove(node); if (doRebalance) { rebalance(); } node.unmarkInMMContainer(); return; } template <typename T, MM2Q::Hook<T> T::*HookPtr> void MM2Q::Container<T, HookPtr>::setConfig(const Config& newConfig) { if (!tailTrackingEnabled_ && newConfig.tailSize > 0) { throw std::invalid_argument( "Cannot turn on tailHitsTracking (cache drop needed)"); } if (tailTrackingEnabled_ && !newConfig.tailSize) { throw std::invalid_argument( "Cannot turn off tailHitsTracking (cache drop needed)"); } lruMutex_->lock_combine([this, &newConfig]() { config_ = newConfig; lruRefreshTime_.store(config_.lruRefreshTime, std::memory_order_relaxed); nextReconfigureTime_ = config_.mmReconfigureIntervalSecs.count() == 0 ? std::numeric_limits<Time>::max() : static_cast<Time>(util::getCurrentTimeSec()) + config_.mmReconfigureIntervalSecs.count(); }); } template <typename T, MM2Q::Hook<T> T::*HookPtr> typename MM2Q::Config MM2Q::Container<T, HookPtr>::getConfig() const { return lruMutex_->lock_combine([this]() { return config_; }); } template <typename T, MM2Q::Hook<T> T::*HookPtr> bool MM2Q::Container<T, HookPtr>::remove(T& node) noexcept { return lruMutex_->lock_combine([this, &node]() { if (!node.isInMMContainer()) { return false; } removeLocked(node); return true; }); } template <typename T, MM2Q::Hook<T> T::*HookPtr> void MM2Q::Container<T, HookPtr>::remove(Iterator& it) noexcept { T& node = *it; XDCHECK(node.isInMMContainer()); ++it; // rebalance should not be triggered inside this remove, because changing the // queues while having the EvictionIterator can cause inconsistency problem // for the iterator. Also, this remove is followed by an insertion into the // same container, which will trigger rebalance. removeLocked(node, /* doRebalance = */ false); } template <typename T, MM2Q::Hook<T> T::*HookPtr> bool MM2Q::Container<T, HookPtr>::replace(T& oldNode, T& newNode) noexcept { return lruMutex_->lock_combine([this, &oldNode, &newNode]() { if (!oldNode.isInMMContainer() || newNode.isInMMContainer()) { return false; } const auto updateTime = getUpdateTime(oldNode); LruType type = getLruType(oldNode); lru_.getList(type).replace(oldNode, newNode); switch (type) { case LruType::Hot: markHot(newNode); break; case LruType::ColdTail: markTail(newNode); // pass through to also mark cold case LruType::Cold: markCold(newNode); break; case LruType::WarmTail: markTail(newNode); break; // warm is indicated by not marking hot or cold case LruType::Warm: break; case LruType::NumTypes: XDCHECK(false); } oldNode.unmarkInMMContainer(); newNode.markInMMContainer(); setUpdateTime(newNode, updateTime); return true; }); } template <typename T, MM2Q::Hook<T> T::*HookPtr> MM2Q::LruType MM2Q::Container<T, HookPtr>::getTailLru(LruType list) const { switch (list) { case LruType::Warm: return LruType::WarmTail; case LruType::Cold: return LruType::ColdTail; default: XDCHECK(false); throw std::invalid_argument("The LRU list does not have a tail list"); } } template <typename T, MM2Q::Hook<T> T::*HookPtr> void MM2Q::Container<T, HookPtr>::adjustTail(LruType list) { auto tailList = getTailLru(list); auto ptr = lru_.getList(list).getTail(); while (ptr && lru_.getList(tailList).size() + 1 <= config_.tailSize) { markTail(*ptr); lru_.getList(list).remove(*ptr); lru_.getList(tailList).linkAtHead(*ptr); ptr = lru_.getList(list).getTail(); } } template <typename T, MM2Q::Hook<T> T::*HookPtr> serialization::MM2QObject MM2Q::Container<T, HookPtr>::saveState() const noexcept { serialization::MM2QConfig configObject; *configObject.lruRefreshTime() = lruRefreshTime_; *configObject.lruRefreshRatio() = config_.lruRefreshRatio; *configObject.updateOnWrite() = config_.updateOnWrite; *configObject.updateOnRead() = config_.updateOnRead; *configObject.hotSizePercent() = config_.hotSizePercent; *configObject.coldSizePercent() = config_.coldSizePercent; *configObject.rebalanceOnRecordAccess() = config_.rebalanceOnRecordAccess; serialization::MM2QObject object; *object.config() = configObject; *object.tailTrackingEnabled() = tailTrackingEnabled_; *object.lrus() = lru_.saveState(); return object; } template <typename T, MM2Q::Hook<T> T::*HookPtr> MMContainerStat MM2Q::Container<T, HookPtr>::getStats() const noexcept { return lruMutex_->lock_combine([this]() { auto* tail = lru_.size() == 0 ? nullptr : lru_.rbegin().get(); auto computeWeightedAccesses = [&](size_t warm, size_t cold) { return (warm * config_.getWarmSizePercent() + cold * config_.coldSizePercent) / 100; }; // note that in the analagous code in MMLru, we return an instance of // std::array<std::uint64_t, 6> and construct the MMContainerStat instance // outside the lock with some other data that is not required to be read // under a lock. This is done there to take advantage of // folly::DistributedMutex's return-value inlining feature which coalesces // the write from the critical section with the atomic operations required // as part of the internal synchronization mechanism. That then transfers // the synchronization signal as well as the return value in one single // cacheline invalidation message. At the time of writing this decision was // based on an implementation-detail aware to the author - 48 bytes can be // coalesced with the synchronization signal in folly::DistributedMutex. // // we cannot do that here because this critical section returns more data // than can be coalesced internally by folly::DistributedMutex (> 48 bytes). // So we construct and return the entire object under the lock. return MMContainerStat{ lru_.size(), tail == nullptr ? 0 : getUpdateTime(*tail), lruRefreshTime_.load(std::memory_order_relaxed), numHotAccesses_, numColdAccesses_, numWarmAccesses_, computeWeightedAccesses(numWarmTailAccesses_, numColdTailAccesses_)}; }); } template <typename T, MM2Q::Hook<T> T::*HookPtr> void MM2Q::Container<T, HookPtr>::reconfigureLocked(const Time& currTime) { if (currTime < nextReconfigureTime_) { return; } nextReconfigureTime_ = currTime + config_.mmReconfigureIntervalSecs.count(); // update LRU refresh time auto stat = getEvictionAgeStatLocked(0); auto lruRefreshTime = std::min( std::max(config_.defaultLruRefreshTime, static_cast<uint32_t>(stat.warmQueueStat.oldestElementAge * config_.lruRefreshRatio)), kLruRefreshTimeCap); lruRefreshTime_.store(lruRefreshTime, std::memory_order_relaxed); } // Iterator Context Implementation template <typename T, MM2Q::Hook<T> T::*HookPtr> MM2Q::Container<T, HookPtr>::Iterator::Iterator( LockHolder l, const typename LruList::Iterator& iter) noexcept : LruList::Iterator(iter), l_(std::move(l)) {} } // namespace cachelib } // namespace facebook