1 // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. 2 // This source code is licensed under both the GPLv2 (found in the 3 // COPYING file in the root directory) and Apache 2.0 License 4 // (found in the LICENSE.Apache file in the root directory). 5 6 #pragma once 7 8 #include <array> 9 #include <cstdint> 10 #include <memory> 11 #include <mutex> 12 13 #include "cache/cache_helpers.h" 14 #include "port/lang.h" 15 #include "rocksdb/cache.h" 16 #include "rocksdb/status.h" 17 #include "rocksdb/system_clock.h" 18 #include "test_util/sync_point.h" 19 #include "util/coding_lean.h" 20 21 namespace ROCKSDB_NAMESPACE { 22 23 // A generic helper object for gathering stats about cache entries by 24 // iterating over them with ApplyToAllEntries. This class essentially 25 // solves the problem of slowing down a Cache with too many stats 26 // collectors that could be sharing stat results, such as from multiple 27 // column families or multiple DBs sharing a Cache. We employ a few 28 // mitigations: 29 // * Only one collector for a particular kind of Stats is alive 30 // for each Cache. This is guaranteed using the Cache itself to hold 31 // the collector. 32 // * A mutex ensures only one thread is gathering stats for this 33 // collector. 34 // * The most recent gathered stats are saved and simply copied to 35 // satisfy requests within a time window (default: 3 minutes) of 36 // completion of the most recent stat gathering. 37 // 38 // Template parameter Stats must be copyable and trivially constructable, 39 // as well as... 40 // concept Stats { 41 // // Notification before applying callback to all entries 42 // void BeginCollection(Cache*, SystemClock*, uint64_t start_time_micros); 43 // // Get the callback to apply to all entries. `callback` 44 // // type must be compatible with Cache::ApplyToAllEntries 45 // callback GetEntryCallback(); 46 // // Notification after applying callback to all entries 47 // void EndCollection(Cache*, SystemClock*, uint64_t end_time_micros); 48 // // Notification that a collection was skipped because of 49 // // sufficiently recent saved results. 50 // void SkippedCollection(); 51 // } 52 template <class Stats> 53 class CacheEntryStatsCollector { 54 public: 55 // Gather and save stats if saved stats are too old. (Use GetStats() to 56 // read saved stats.) 57 // 58 // Maximum allowed age for a "hit" on saved results is determined by the 59 // two interval parameters. Both set to 0 forces a re-scan. For example 60 // with min_interval_seconds=300 and min_interval_factor=100, if the last 61 // scan took 10s, we would only rescan ("miss") if the age in seconds of 62 // the saved results is > max(300, 100*10). 63 // Justification: scans can vary wildly in duration, e.g. from 0.02 sec 64 // to as much as 20 seconds, so we want to be able to cap the absolute 65 // and relative frequency of scans. CollectStats(int min_interval_seconds,int min_interval_factor)66 void CollectStats(int min_interval_seconds, int min_interval_factor) { 67 // Waits for any pending reader or writer (collector) 68 std::lock_guard<std::mutex> lock(working_mutex_); 69 70 uint64_t max_age_micros = 71 static_cast<uint64_t>(std::max(min_interval_seconds, 0)) * 1000000U; 72 73 if (last_end_time_micros_ > last_start_time_micros_ && 74 min_interval_factor > 0) { 75 max_age_micros = std::max( 76 max_age_micros, min_interval_factor * (last_end_time_micros_ - 77 last_start_time_micros_)); 78 } 79 80 uint64_t start_time_micros = clock_->NowMicros(); 81 if ((start_time_micros - last_end_time_micros_) > max_age_micros) { 82 last_start_time_micros_ = start_time_micros; 83 working_stats_.BeginCollection(cache_, clock_, start_time_micros); 84 85 cache_->ApplyToAllEntries(working_stats_.GetEntryCallback(), {}); 86 TEST_SYNC_POINT_CALLBACK( 87 "CacheEntryStatsCollector::GetStats:AfterApplyToAllEntries", nullptr); 88 89 uint64_t end_time_micros = clock_->NowMicros(); 90 last_end_time_micros_ = end_time_micros; 91 working_stats_.EndCollection(cache_, clock_, end_time_micros); 92 } else { 93 working_stats_.SkippedCollection(); 94 } 95 96 // Save so that we don't need to wait for an outstanding collection in 97 // order to make of copy of the last saved stats 98 std::lock_guard<std::mutex> lock2(saved_mutex_); 99 saved_stats_ = working_stats_; 100 } 101 102 // Gets saved stats, regardless of age GetStats(Stats * stats)103 void GetStats(Stats *stats) { 104 std::lock_guard<std::mutex> lock(saved_mutex_); 105 *stats = saved_stats_; 106 } 107 GetCache()108 Cache *GetCache() const { return cache_; } 109 110 // Gets or creates a shared instance of CacheEntryStatsCollector in the 111 // cache itself, and saves into `ptr`. This shared_ptr will hold the 112 // entry in cache until all refs are destroyed. GetShared(Cache * cache,SystemClock * clock,std::shared_ptr<CacheEntryStatsCollector> * ptr)113 static Status GetShared(Cache *cache, SystemClock *clock, 114 std::shared_ptr<CacheEntryStatsCollector> *ptr) { 115 std::array<uint64_t, 3> cache_key_data{ 116 {// First 16 bytes == md5 of class name 117 0x7eba5a8fb5437c90U, 0x8ca68c9b11655855U, 118 // Last 8 bytes based on a function pointer to make unique for each 119 // template instantiation 120 reinterpret_cast<uint64_t>(&CacheEntryStatsCollector::GetShared)}}; 121 Slice cache_key = GetSlice(&cache_key_data); 122 123 Cache::Handle *h = cache->Lookup(cache_key); 124 if (h == nullptr) { 125 // Not yet in cache, but Cache doesn't provide a built-in way to 126 // avoid racing insert. So we double-check under a shared mutex, 127 // inspired by TableCache. 128 STATIC_AVOID_DESTRUCTION(std::mutex, static_mutex); 129 std::lock_guard<std::mutex> lock(static_mutex); 130 131 h = cache->Lookup(cache_key); 132 if (h == nullptr) { 133 auto new_ptr = new CacheEntryStatsCollector(cache, clock); 134 // TODO: non-zero charge causes some tests that count block cache 135 // usage to go flaky. Fix the problem somehow so we can use an 136 // accurate charge. 137 size_t charge = 0; 138 Status s = cache->Insert(cache_key, new_ptr, charge, Deleter, &h, 139 Cache::Priority::HIGH); 140 if (!s.ok()) { 141 assert(h == nullptr); 142 delete new_ptr; 143 return s; 144 } 145 } 146 } 147 // If we reach here, shared entry is in cache with handle `h`. 148 assert(cache->GetDeleter(h) == Deleter); 149 150 // Build an aliasing shared_ptr that keeps `ptr` in cache while there 151 // are references. 152 *ptr = MakeSharedCacheHandleGuard<CacheEntryStatsCollector>(cache, h); 153 return Status::OK(); 154 } 155 156 private: CacheEntryStatsCollector(Cache * cache,SystemClock * clock)157 explicit CacheEntryStatsCollector(Cache *cache, SystemClock *clock) 158 : saved_stats_(), 159 working_stats_(), 160 last_start_time_micros_(0), 161 last_end_time_micros_(/*pessimistic*/ 10000000), 162 cache_(cache), 163 clock_(clock) {} 164 Deleter(const Slice &,void * value)165 static void Deleter(const Slice &, void *value) { 166 delete static_cast<CacheEntryStatsCollector *>(value); 167 } 168 169 std::mutex saved_mutex_; 170 Stats saved_stats_; 171 172 std::mutex working_mutex_; 173 Stats working_stats_; 174 uint64_t last_start_time_micros_; 175 uint64_t last_end_time_micros_; 176 177 Cache *const cache_; 178 SystemClock *const clock_; 179 }; 180 181 } // namespace ROCKSDB_NAMESPACE 182