Coverage Report

Created: 2026-02-14 06:58

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