Skip to content

Commit 6885c8f

Browse files
Stuart Clarkmeta-codesync[bot]
authored andcommitted
Fix NVM latency tracking to exclude fast-path lookups
Summary: NVM lookup and insert latency stats (nvmLookupLatency_, nvmInsertLatency_) were being recorded for operations that never reach the Navy device I/O layer, causing P50 to appear flat when actual device latency changes. For lookups, the LatencyTracker was created before the bloom filter fast-path check, so the majority of lookups that are rejected by couldExist() recorded near-zero latency, dominating P50. Coalesced lookups (waiters on an existing GetCtx) were also silently counted. For inserts, the tracker started before early-exit checks (disabled, expired, tombstone, encode failure), similarly polluting the stats. Move the LatencyTracker creation to just before the actual async Navy operation begins, so only real device I/O is measured. Also adds unit tests for LatencyTracker and PercentileStats, which previously had no test coverage. Reviewed By: rlyerly, pbhandar2 Differential Revision: D101208795 fbshipit-source-id: dfce45af613d181d0b31486e544f12552e8ca9f7
1 parent f89adea commit 6885c8f

2 files changed

Lines changed: 149 additions & 3 deletions

File tree

cachelib/allocator/nvmcache/NvmCache.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,6 @@ typename NvmCache<C>::WriteHandle NvmCache<C>::find(HashedKey hk) {
713713
return WriteHandle{};
714714
}
715715

716-
util::LatencyTracker tracker(stats().nvmLookupLatency_);
717716
stats().numNvmGets.inc();
718717

719718
auto shard = getShardForKey(hk);
@@ -815,7 +814,9 @@ typename NvmCache<C>::WriteHandle NvmCache<C>::find(HashedKey hk) {
815814
return hdl;
816815
}
817816

818-
// create a context
817+
// create a context — start latency tracking here so bloom filter
818+
// fast-path misses and coalesced lookups are excluded
819+
util::LatencyTracker tracker(stats().nvmLookupLatency_);
819820
auto newCtx = std::make_unique<GetCtx>(
820821
*this, hk.key(), std::move(waitContext), std::move(tracker));
821822
auto res =
@@ -1210,7 +1211,6 @@ std::unique_ptr<NvmItem> NvmCache<C>::makeNvmItem(const Item& item,
12101211

12111212
template <typename C>
12121213
void NvmCache<C>::put(Item& item, PutToken token) {
1213-
util::LatencyTracker tracker(stats().nvmInsertLatency_);
12141214
HashedKey hk{item.getKey()};
12151215

12161216
// for regular items that can only write to nvmcache upon eviction, we
@@ -1263,6 +1263,9 @@ void NvmCache<C>::put(Item& item, PutToken token) {
12631263
auto val = folly::ByteRange{iobuf.data(), iobuf.length()};
12641264

12651265
auto shard = getShardForKey(hk);
1266+
// start latency tracking here so early-exit paths (disabled, expired,
1267+
// tombstone, encode failure) are excluded from the latency stats
1268+
util::LatencyTracker tracker(stats().nvmInsertLatency_);
12661269
auto& putContexts = putContexts_[shard];
12671270
auto& ctx = putContexts.createContext(item.getKey(), std::move(iobuf),
12681271
std::move(tracker));
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
#include <gtest/gtest.h>
18+
19+
#include <chrono>
20+
#include <thread>
21+
22+
#include "cachelib/common/PercentileStats.h"
23+
24+
namespace facebook {
25+
namespace cachelib {
26+
namespace tests {
27+
28+
namespace {
29+
// Measure wall-clock nanoseconds across a sleep so assertions are based on
30+
// what actually elapsed, not what we requested. This makes the tests
31+
// reliable even on overloaded hosts where sleep_for overshoots.
32+
uint64_t sleepAndMeasureNs(std::chrono::milliseconds dur) {
33+
auto before = std::chrono::steady_clock::now();
34+
std::this_thread::sleep_for(dur);
35+
auto after = std::chrono::steady_clock::now();
36+
return std::chrono::duration_cast<std::chrono::nanoseconds>(after - before)
37+
.count();
38+
}
39+
} // namespace
40+
41+
TEST(LatencyTracker, DefaultConstructedRecordsNothing) {
42+
util::PercentileStats stats;
43+
{
44+
util::LatencyTracker tracker;
45+
// default-constructed, no stats pointer — should not record
46+
}
47+
auto est = stats.estimate();
48+
EXPECT_EQ(0, est.p50);
49+
}
50+
51+
TEST(LatencyTracker, RecordsLatencyOnDestruction) {
52+
util::PercentileStats stats;
53+
uint64_t elapsedNs;
54+
{
55+
util::LatencyTracker tracker(stats);
56+
elapsedNs = sleepAndMeasureNs(std::chrono::milliseconds(10));
57+
}
58+
auto est = stats.estimate();
59+
// recorded value should be in the same ballpark as what we measured
60+
EXPECT_GE(est.p50, elapsedNs / 2);
61+
EXPECT_LE(est.p50, elapsedNs * 2);
62+
}
63+
64+
TEST(LatencyTracker, MoveConstructedRecordsOnce) {
65+
util::PercentileStats stats;
66+
uint64_t elapsedNs;
67+
{
68+
util::LatencyTracker original(stats);
69+
elapsedNs = sleepAndMeasureNs(std::chrono::milliseconds(10));
70+
// move into a new tracker — original becomes inert
71+
util::LatencyTracker moved(std::move(original));
72+
// destroying original here should NOT record
73+
}
74+
// moved tracker's destructor records exactly once
75+
auto est = stats.estimate();
76+
EXPECT_GE(est.p50, elapsedNs / 2);
77+
}
78+
79+
TEST(LatencyTracker, MoveAssignedRecordsOnce) {
80+
util::PercentileStats stats;
81+
uint64_t elapsedNs;
82+
{
83+
util::LatencyTracker original(stats);
84+
elapsedNs = sleepAndMeasureNs(std::chrono::milliseconds(10));
85+
util::LatencyTracker target;
86+
target = std::move(original);
87+
// original is now inert; target holds the tracking state
88+
}
89+
auto est = stats.estimate();
90+
EXPECT_GE(est.p50, elapsedNs / 2);
91+
}
92+
93+
TEST(LatencyTracker, MoveAssignToActiveRecordsBoth) {
94+
util::PercentileStats stats1;
95+
util::PercentileStats stats2;
96+
uint64_t elapsedNs;
97+
{
98+
util::LatencyTracker tracker1(stats1);
99+
util::LatencyTracker tracker2(stats2);
100+
elapsedNs = sleepAndMeasureNs(std::chrono::milliseconds(10));
101+
// move-assigning tracker2 into tracker1 should first record tracker1's
102+
// latency (via destructor), then transfer tracker2's state
103+
tracker1 = std::move(tracker2);
104+
}
105+
// stats1 recorded when tracker1 was destroyed by move-assignment
106+
auto est1 = stats1.estimate();
107+
EXPECT_GE(est1.p50, elapsedNs / 2);
108+
// stats2 recorded when tracker1 (now holding tracker2's state) was destroyed
109+
auto est2 = stats2.estimate();
110+
EXPECT_GE(est2.p50, elapsedNs / 2);
111+
}
112+
113+
TEST(PercentileStats, TrackValueAndEstimate) {
114+
util::PercentileStats stats;
115+
auto now = std::chrono::steady_clock::now();
116+
for (int i = 0; i < 100; ++i) {
117+
stats.trackValue(1000.0, now);
118+
}
119+
auto est = stats.estimate();
120+
EXPECT_EQ(1000, est.p50);
121+
EXPECT_EQ(1000, est.avg);
122+
}
123+
124+
TEST(PercentileStats, VisitQuantileEstimates) {
125+
util::PercentileStats::Estimates est;
126+
est.p50 = 42;
127+
est.p99 = 99;
128+
129+
std::map<std::string, double> visited;
130+
auto visitor = [&](folly::StringPiece name, double val) {
131+
visited[name.str()] = val;
132+
};
133+
134+
util::PercentileStats::visitQuantileEstimates(visitor, est, "test_stat");
135+
EXPECT_EQ(42, visited["test_stat_p50"]);
136+
EXPECT_EQ(99, visited["test_stat_p99"]);
137+
// all 15 fields should be visited (avg, min, p5..p999999, max)
138+
EXPECT_EQ(15, visited.size());
139+
}
140+
141+
} // namespace tests
142+
} // namespace cachelib
143+
} // namespace facebook

0 commit comments

Comments
 (0)