Skip to content

Commit 5e3917a

Browse files
pbhandar2meta-codesync[bot]
authored andcommitted
Implement a map to track last accessed timestamp
Summary: The high-level intention of this stack of diffs is to implement retention time enforcement in CacheLib caches by tracking the time-to-access (TTA) of items in the cache. The diffs implements a map that tracks the last accessed timestamp of items that are being evicted from DRAM but a copy already exists in NVM which contains a stale last accessed timestamp. Reviewed By: AlnisM Differential Revision: D95973518 fbshipit-source-id: 44ece9a75c61ba391406526eadf5ee5e6dfdfc1f
1 parent a212d4f commit 5e3917a

2 files changed

Lines changed: 456 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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

Comments
 (0)