|
| 1 | +/* |
| 2 | + * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +#pragma once |
| 18 | + |
| 19 | +#include <folly/container/F14Map.h> |
| 20 | +#include <folly/lang/Align.h> |
| 21 | + |
| 22 | +#include <atomic> |
| 23 | +#include <mutex> |
| 24 | +#include <optional> |
| 25 | +#include <stdexcept> |
| 26 | +#include <vector> |
| 27 | + |
| 28 | +#include "cachelib/common/AtomicCounter.h" |
| 29 | +#include "cachelib/common/Utils.h" |
| 30 | + |
| 31 | +namespace facebook::cachelib { |
| 32 | + |
| 33 | +// Sharded concurrent map from key hash to last-access timestamp. |
| 34 | +// Tracks the most recent DRAM access time for NVM-resident items so that |
| 35 | +// BlockCache reinsertion and time-to-access (TTA) stats use fresher |
| 36 | +// timestamps than the on-disk EntryDesc value. |
| 37 | +// |
| 38 | +// Written on DRAM eviction of NvmClean BlockCache items, read during |
| 39 | +// BlockCache region reclaim and NVM-to-DRAM promotion, and cleaned up |
| 40 | +// when an item leaves NVM. Only BlockCache items are tracked (BigHash |
| 41 | +// items don't reinsert). Not updated on every DRAM access to avoid |
| 42 | +// mutex overhead on the read path. |
| 43 | +class AccessTimeMap { |
| 44 | + public: |
| 45 | + // maxSize: approximate upper bound on total entries across all shards. |
| 46 | + // Enforced per-shard as ceil(maxSize / numShards). 0 means unbounded. |
| 47 | + explicit AccessTimeMap(size_t numShards, size_t maxSize = 0) |
| 48 | + : shards_(numShards), |
| 49 | + maxEntriesPerShard_(computeMaxEntriesPerShard(numShards, maxSize)) {} |
| 50 | + |
| 51 | + // Store a timestamp for the given key hash. |
| 52 | + // If inserting a new key causes the shard to exceed capacity, an arbitrary |
| 53 | + // existing entry in the same shard is evicted to restore the limit. |
| 54 | + void set(uint64_t keyHash, uint32_t accessTimeSecs) { |
| 55 | + sets_.inc(); |
| 56 | + auto& shard = getShard(keyHash); |
| 57 | + std::lock_guard l(shard.mutex_); |
| 58 | + auto [it, inserted] = shard.map_.insert_or_assign(keyHash, accessTimeSecs); |
| 59 | + if (inserted) { |
| 60 | + // Evict an arbitrary entry (not the one we just inserted). The victim |
| 61 | + // is not LRU/FIFO — F14 iteration order is unspecified. This is |
| 62 | + // acceptable because the map is a best-effort timestamp cache. |
| 63 | + if (maxEntriesPerShard_ > 0 && shard.map_.size() > maxEntriesPerShard_) { |
| 64 | + auto victim = shard.map_.begin(); |
| 65 | + if (victim == it) { |
| 66 | + ++victim; |
| 67 | + } |
| 68 | + shard.map_.erase(victim); |
| 69 | + evictions_.inc(); |
| 70 | + } else { |
| 71 | + size_.fetch_add(1, std::memory_order_relaxed); |
| 72 | + } |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + // Retrieve the timestamp for the given key hash without removing it. |
| 77 | + std::optional<uint32_t> get(uint64_t keyHash) const { |
| 78 | + gets_.inc(); |
| 79 | + auto& shard = getShard(keyHash); |
| 80 | + std::lock_guard l(shard.mutex_); |
| 81 | + auto it = shard.map_.find(keyHash); |
| 82 | + if (it == shard.map_.end()) { |
| 83 | + misses_.inc(); |
| 84 | + return std::nullopt; |
| 85 | + } |
| 86 | + hits_.inc(); |
| 87 | + return it->second; |
| 88 | + } |
| 89 | + |
| 90 | + // Retrieve and remove the timestamp for the given key hash. |
| 91 | + // Returns std::nullopt if not found. |
| 92 | + std::optional<uint32_t> getAndRemove(uint64_t keyHash) { |
| 93 | + getAndRemoves_.inc(); |
| 94 | + auto& shard = getShard(keyHash); |
| 95 | + std::lock_guard l(shard.mutex_); |
| 96 | + auto it = shard.map_.find(keyHash); |
| 97 | + if (it == shard.map_.end()) { |
| 98 | + misses_.inc(); |
| 99 | + return std::nullopt; |
| 100 | + } |
| 101 | + auto val = it->second; |
| 102 | + shard.map_.erase(it); |
| 103 | + size_.fetch_sub(1, std::memory_order_relaxed); |
| 104 | + hits_.inc(); |
| 105 | + return val; |
| 106 | + } |
| 107 | + |
| 108 | + // Remove the entry for the given key hash, if present. |
| 109 | + void remove(uint64_t keyHash) { |
| 110 | + removes_.inc(); |
| 111 | + auto& shard = getShard(keyHash); |
| 112 | + std::lock_guard l(shard.mutex_); |
| 113 | + if (shard.map_.erase(keyHash)) { |
| 114 | + size_.fetch_sub(1, std::memory_order_relaxed); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + // Approximate total number of entries. O(1), no locking. |
| 119 | + // May be slightly stale under concurrent modifications due to relaxed |
| 120 | + // memory ordering, but avoids the contention of locking all shards. |
| 121 | + size_t size() const { return size_.load(std::memory_order_relaxed); } |
| 122 | + |
| 123 | + // Counters are read without a global barrier, so a snapshot may be |
| 124 | + // internally inconsistent (e.g. hits + misses > gets). |
| 125 | + void getCounters(const util::CounterVisitor& visitor) const { |
| 126 | + visitor("navy_atm_size", size_.load(std::memory_order_relaxed), |
| 127 | + util::CounterVisitor::CounterType::COUNT); |
| 128 | + visitor("navy_atm_sets", sets_.get(), |
| 129 | + util::CounterVisitor::CounterType::RATE); |
| 130 | + visitor("navy_atm_gets", gets_.get(), |
| 131 | + util::CounterVisitor::CounterType::RATE); |
| 132 | + visitor("navy_atm_get_and_removes", getAndRemoves_.get(), |
| 133 | + util::CounterVisitor::CounterType::RATE); |
| 134 | + visitor("navy_atm_removes", removes_.get(), |
| 135 | + util::CounterVisitor::CounterType::RATE); |
| 136 | + visitor("navy_atm_hits", hits_.get(), |
| 137 | + util::CounterVisitor::CounterType::RATE); |
| 138 | + visitor("navy_atm_misses", misses_.get(), |
| 139 | + util::CounterVisitor::CounterType::RATE); |
| 140 | + visitor("navy_atm_evictions", evictions_.get(), |
| 141 | + util::CounterVisitor::CounterType::RATE); |
| 142 | + } |
| 143 | + |
| 144 | + private: |
| 145 | + // Each shard is aligned to a cache line boundary to prevent false sharing |
| 146 | + // between shards accessed by different threads. |
| 147 | + struct alignas(folly::hardware_destructive_interference_size) Shard { |
| 148 | + mutable std::mutex mutex_; |
| 149 | + folly::F14FastMap<uint64_t, uint32_t> map_; |
| 150 | + }; |
| 151 | + |
| 152 | + static size_t computeMaxEntriesPerShard(size_t numShards, size_t maxSize) { |
| 153 | + if (numShards == 0) { |
| 154 | + throw std::invalid_argument("AccessTimeMap requires numShards > 0"); |
| 155 | + } |
| 156 | + return maxSize > 0 ? (maxSize + numShards - 1) / numShards : 0; |
| 157 | + } |
| 158 | + |
| 159 | + Shard& getShard(uint64_t keyHash) { |
| 160 | + return shards_[keyHash % shards_.size()]; |
| 161 | + } |
| 162 | + |
| 163 | + const Shard& getShard(uint64_t keyHash) const { |
| 164 | + return shards_[keyHash % shards_.size()]; |
| 165 | + } |
| 166 | + |
| 167 | + std::vector<Shard> shards_; |
| 168 | + const size_t maxEntriesPerShard_{0}; |
| 169 | + std::atomic<size_t> size_{0}; |
| 170 | + |
| 171 | + mutable TLCounter sets_; |
| 172 | + mutable TLCounter gets_; |
| 173 | + mutable TLCounter getAndRemoves_; |
| 174 | + mutable TLCounter removes_; |
| 175 | + mutable TLCounter hits_; |
| 176 | + mutable TLCounter misses_; |
| 177 | + mutable TLCounter evictions_; |
| 178 | +}; |
| 179 | + |
| 180 | +} // namespace facebook::cachelib |
0 commit comments