@@ -63,6 +63,63 @@ struct ConstantLookup : public Lookup {
6363 }
6464};
6565
66+ // A Lookup that tracks calls and can return nothing (simulating missing facts)
67+ struct TrackingLookup : public Lookup {
68+ int idByKeyCalls = 0 ;
69+ int typeByIdCalls = 0 ;
70+ int factByIdCalls = 0 ;
71+ bool returnEmpty = false ;
72+
73+ Id idByKey (Pid, folly::ByteRange) override {
74+ ++idByKeyCalls;
75+ return returnEmpty ? Id::invalid () : Id::lowest ();
76+ }
77+ Pid typeById (Id) override {
78+ ++typeByIdCalls;
79+ return returnEmpty ? Pid::invalid () : Pid::lowest ();
80+ }
81+ bool factById (Id, std::function<void (Pid, Fact::Clause)> f) override {
82+ ++factByIdCalls;
83+ if (returnEmpty) {
84+ return false ;
85+ }
86+ static unsigned char clause[] = " Helloworld" ;
87+ f (Pid::lowest (), {clause, 5 , 5 });
88+ return true ;
89+ }
90+
91+ Id startingId () const override {
92+ return Id::lowest ();
93+ }
94+ Id firstFreeId () const override {
95+ return Id::lowest () + 100 ;
96+ }
97+ std::unique_ptr<FactIterator> enumerate(Id, Id) override {
98+ return std::make_unique<EmptyIterator>();
99+ }
100+ std::unique_ptr<FactIterator> enumerateBack (Id, Id) override {
101+ return std::make_unique<EmptyIterator>();
102+ }
103+ Interval count (Pid) const override {
104+ return 1 ;
105+ }
106+ std::unique_ptr<FactIterator>
107+ seek (Pid, folly::ByteRange, std::optional<Fact::Ref>) override {
108+ return std::make_unique<EmptyIterator>();
109+ }
110+ std::unique_ptr<FactIterator> seekWithinSection (
111+ Pid,
112+ folly::ByteRange,
113+ Id,
114+ Id,
115+ std::optional<Fact::Ref>) override {
116+ return std::make_unique<EmptyIterator>();
117+ }
118+ UsetId getOwner (Id) override {
119+ return INVALID_USET;
120+ }
121+ };
122+
66123struct CacheTest : testing::Test {
67124 template <typename Base, typename F>
68125 void setup (Base&& b, F&& init) {
@@ -78,6 +135,24 @@ struct CacheTest : testing::Test {
78135
79136 void typeById_miss (size_t miss, size_t shards);
80137
138+ TrackingLookup* setupTracking (
139+ size_t capacity = 10 * 1024 ,
140+ size_t shards = 1 ,
141+ bool returnEmpty = false ) {
142+ auto tracking = std::make_unique<TrackingLookup>();
143+ tracking->returnEmpty = returnEmpty;
144+ auto * trackPtr = tracking.get ();
145+ stats = std::make_shared<LookupCache::Stats>();
146+ base = std::move (tracking);
147+ LookupCache::Options opts;
148+ opts.capacity = capacity;
149+ opts.shards = shards;
150+ opts.touched_buffer_size = 128 ;
151+ cache = std::make_unique<LookupCache>(opts, stats);
152+ lookup = std::make_unique<LookupCache::Anchor>(cache->anchor (trackPtr));
153+ return trackPtr;
154+ }
155+
81156 std::shared_ptr<LookupCache::Stats> stats;
82157 std::unique_ptr<Lookup> base;
83158 std::unique_ptr<LookupCache> cache;
@@ -193,3 +268,190 @@ TEST_F(CacheTest, nested) {
193268 }
194269 });
195270}
271+
272+ TEST_F (CacheTest, TypeByIdMissRecordsStats) {
273+ auto * trackPtr = setupTracking ();
274+ auto type = lookup->typeById (Id::lowest ());
275+ EXPECT_EQ (type, Pid::lowest ());
276+ EXPECT_EQ (trackPtr->typeByIdCalls , 1 );
277+ auto s = stats->read ();
278+ EXPECT_EQ (s[LookupCache::Stats::typeById_misses], 1 );
279+ EXPECT_EQ (s[LookupCache::Stats::typeById_hits], 0 );
280+ }
281+
282+ TEST_F (CacheTest, TypeByIdHitRecordsStats) {
283+ auto * trackPtr = setupTracking ();
284+ lookup->typeById (Id::lowest ());
285+ auto type = lookup->typeById (Id::lowest ());
286+ EXPECT_EQ (type, Pid::lowest ());
287+ EXPECT_EQ (trackPtr->typeByIdCalls , 1 );
288+ auto s = stats->read ();
289+ EXPECT_EQ (s[LookupCache::Stats::typeById_misses], 1 );
290+ EXPECT_EQ (s[LookupCache::Stats::typeById_hits], 1 );
291+ }
292+
293+ TEST_F (CacheTest, TypeByIdFailureRecordsStats) {
294+ setupTracking (10 * 1024 , 1 , true );
295+ auto type = lookup->typeById (Id::lowest ());
296+ EXPECT_EQ (type, Pid::invalid ());
297+ auto s = stats->read ();
298+ EXPECT_EQ (s[LookupCache::Stats::typeById_failures], 1 );
299+ }
300+
301+ TEST_F (CacheTest, FactByIdMissAndHitRecordsStats) {
302+ auto * trackPtr = setupTracking ();
303+ Pid capturedType = Pid::invalid ();
304+ bool found = lookup->factById (
305+ Id::lowest (), [&](Pid t, Fact::Clause) { capturedType = t; });
306+ EXPECT_TRUE (found);
307+ EXPECT_EQ (capturedType, Pid::lowest ());
308+ EXPECT_EQ (trackPtr->factByIdCalls , 1 );
309+ capturedType = Pid::invalid ();
310+ found = lookup->factById (
311+ Id::lowest (), [&](Pid t, Fact::Clause) { capturedType = t; });
312+ EXPECT_TRUE (found);
313+ EXPECT_EQ (capturedType, Pid::lowest ());
314+ EXPECT_EQ (trackPtr->factByIdCalls , 1 );
315+ auto s = stats->read ();
316+ EXPECT_EQ (s[LookupCache::Stats::factById_misses], 1 );
317+ EXPECT_EQ (s[LookupCache::Stats::factById_hits], 1 );
318+ }
319+
320+ TEST_F (CacheTest, FactByIdFailureRecordsStats) {
321+ setupTracking (10 * 1024 , 1 , true );
322+ bool found = lookup->factById (Id::lowest (), [](Pid, Fact::Clause) {});
323+ EXPECT_FALSE (found);
324+ auto s = stats->read ();
325+ EXPECT_EQ (s[LookupCache::Stats::factById_failures], 1 );
326+ }
327+
328+ TEST_F (CacheTest, IdByKeyMissAndHitRecordsStats) {
329+ auto * trackPtr = setupTracking ();
330+ unsigned char keyData[] = " testkey" ;
331+ folly::ByteRange key (keyData, 7 );
332+ auto id = lookup->idByKey (Pid::lowest (), key);
333+ EXPECT_EQ (id, Id::lowest ());
334+ EXPECT_EQ (trackPtr->idByKeyCalls , 1 );
335+ id = lookup->idByKey (Pid::lowest (), key);
336+ EXPECT_EQ (id, Id::lowest ());
337+ EXPECT_EQ (trackPtr->idByKeyCalls , 1 );
338+ auto s = stats->read ();
339+ EXPECT_EQ (s[LookupCache::Stats::idByKey_misses], 1 );
340+ EXPECT_EQ (s[LookupCache::Stats::idByKey_hits], 1 );
341+ }
342+
343+ TEST_F (CacheTest, IdByKeyFailureRecordsStats) {
344+ setupTracking (10 * 1024 , 1 , true );
345+ unsigned char keyData[] = " testkey" ;
346+ folly::ByteRange key (keyData, 7 );
347+ auto id = lookup->idByKey (Pid::lowest (), key);
348+ EXPECT_EQ (id, Id::invalid ());
349+ auto s = stats->read ();
350+ EXPECT_EQ (s[LookupCache::Stats::idByKey_failures], 1 );
351+ }
352+
353+ TEST_F (CacheTest, ReadAndResetCountersResetsOnlyCounters) {
354+ setupTracking ();
355+ lookup->typeById (Id::lowest ());
356+ lookup->typeById (Id::lowest ());
357+ auto s1 = stats->readAndResetCounters ();
358+ EXPECT_EQ (s1[LookupCache::Stats::typeById_misses], 1 );
359+ EXPECT_EQ (s1[LookupCache::Stats::typeById_hits], 1 );
360+ EXPECT_GT (s1[LookupCache::Stats::factBytes], 0 );
361+ EXPECT_EQ (s1[LookupCache::Stats::factCount], 1 );
362+ auto s2 = stats->read ();
363+ EXPECT_EQ (s2[LookupCache::Stats::typeById_misses], 0 );
364+ EXPECT_EQ (s2[LookupCache::Stats::typeById_hits], 0 );
365+ EXPECT_GT (s2[LookupCache::Stats::factBytes], 0 );
366+ EXPECT_EQ (s2[LookupCache::Stats::factCount], 1 );
367+ }
368+
369+ TEST_F (CacheTest, EvictionWhenCapacityExceeded) {
370+ setupTracking (256 );
371+ for (size_t i = 0 ; i < 20 ; ++i) {
372+ lookup->typeById (Id::lowest () + i);
373+ }
374+ auto s = stats->read ();
375+ EXPECT_LE (s[LookupCache::Stats::factBytes], 256 );
376+ }
377+
378+ TEST_F (CacheTest, UpgradeFromTypeToFullReplacesEntry) {
379+ auto * trackPtr = setupTracking ();
380+ lookup->typeById (Id::lowest ());
381+ EXPECT_EQ (trackPtr->typeByIdCalls , 1 );
382+ bool found = lookup->factById (Id::lowest (), [](Pid, Fact::Clause) {});
383+ EXPECT_TRUE (found);
384+ EXPECT_EQ (trackPtr->factByIdCalls , 1 );
385+ auto s = stats->read ();
386+ EXPECT_EQ (s[LookupCache::Stats::factById_deletes], 1 );
387+ found = lookup->factById (Id::lowest (), [](Pid, Fact::Clause) {});
388+ EXPECT_TRUE (found);
389+ EXPECT_EQ (trackPtr->factByIdCalls , 1 );
390+ }
391+
392+ TEST_F (CacheTest, FIFOReplacementPolicySkipsTouch) {
393+ auto * trackPtr = setupTracking ();
394+ auto fifoAnchor =
395+ cache->anchor (trackPtr, LookupCache::Anchor::ReplacementPolicy::FIFO);
396+ fifoAnchor.typeById (Id::lowest ());
397+ auto type = fifoAnchor.typeById (Id::lowest ());
398+ EXPECT_EQ (type, Pid::lowest ());
399+ EXPECT_EQ (trackPtr->typeByIdCalls , 1 );
400+ auto s = stats->read ();
401+ EXPECT_EQ (s[LookupCache::Stats::typeById_hits], 1 );
402+ }
403+
404+ TEST_F (CacheTest, DelegatesEnumerateToBase) {
405+ setupTracking ();
406+ auto iter = lookup->enumerate(Id::lowest (), Id::lowest () + 10 );
407+ ASSERT_NE (iter, nullptr );
408+ EXPECT_FALSE (bool (iter->get ()));
409+ auto iterBack = lookup->enumerateBack (Id::lowest () + 10 , Id::lowest ());
410+ ASSERT_NE (iterBack, nullptr );
411+ EXPECT_FALSE (bool (iterBack->get ()));
412+ unsigned char prefix[] = " test" ;
413+ auto seekIter = lookup->seek (Pid::lowest (), folly::ByteRange (prefix, 4 ), {});
414+ ASSERT_NE (seekIter, nullptr );
415+ EXPECT_FALSE (bool (seekIter->get ()));
416+ }
417+
418+ TEST_F (CacheTest, BulkStoreInsertsMultipleFacts) {
419+ auto * trackPtr = setupTracking ();
420+ unsigned char data[] = " keyvalue" ;
421+ cache->withBulkStore ([&](Store& store) {
422+ for (size_t i = 0 ; i < 5 ; ++i) {
423+ Fact::Ref ref;
424+ ref.id = Id::lowest () + i;
425+ ref.type = Pid::lowest ();
426+ ref.clause = Fact::Clause::from (folly::ByteRange (data, 8 ), 3 );
427+ store.insert (ref);
428+ }
429+ });
430+ for (size_t i = 0 ; i < 5 ; ++i) {
431+ bool found = lookup->factById (Id::lowest () + i, [](Pid, Fact::Clause) {});
432+ EXPECT_TRUE (found);
433+ }
434+ EXPECT_EQ (trackPtr->factByIdCalls , 0 );
435+ auto s = stats->read ();
436+ EXPECT_EQ (s[LookupCache::Stats::factById_hits], 5 );
437+ }
438+
439+ TEST_F (CacheTest, FactTooLargeForCacheIsNotInserted) {
440+ auto * trackPtr = setupTracking (1 );
441+ lookup->typeById (Id::lowest ());
442+ lookup->typeById (Id::lowest ());
443+ EXPECT_EQ (trackPtr->typeByIdCalls , 2 );
444+ auto s = stats->read ();
445+ EXPECT_EQ (s[LookupCache::Stats::typeById_hits], 0 );
446+ EXPECT_EQ (s[LookupCache::Stats::typeById_misses], 2 );
447+ }
448+
449+ TEST_F (CacheTest, ZeroShardsUsesDirectTouch) {
450+ setupTracking (10 * 1024 , 0 );
451+ lookup->typeById (Id::lowest ());
452+ auto type = lookup->typeById (Id::lowest ());
453+ EXPECT_EQ (type, Pid::lowest ());
454+ auto s = stats->read ();
455+ EXPECT_EQ (s[LookupCache::Stats::typeById_hits], 1 );
456+ EXPECT_EQ (s[LookupCache::Stats::typeById_misses], 1 );
457+ }
0 commit comments