1717
1818#include " topkeys.h"
1919#include " settings.h"
20+
21+ #include < platform/sysinfo.h>
22+
23+ #include < folly/concurrency/CacheLocality.h>
2024#include < inttypes.h>
2125#include < nlohmann/json.hpp>
2226#include < stdlib.h>
2327#include < sys/types.h>
28+
2429#include < algorithm>
2530#include < cstring>
2631#include < gsl/gsl>
32+ #include < set>
2733#include < stdexcept>
2834
2935/*
3036 * Implementation Details
3137 *
3238 * === TopKeys ===
3339 *
34- * The TopKeys class is split into NUM_SHARDS shards, each which owns
35- * 1/NUM_SHARDS of the keyspace. This is to allow some level of
36- * concurrent access - each shard has a mutex guarding all access.
37- * Other than that the TopKeys class is pretty uninteresting - it
38- * simply passes on requests to the correct Shard, and when statistics
39- * are requested it aggregates information from each shard.
40+ * The TopKeys class is split into shards for performance reasons. We create one
41+ * shard per (logical) core to:
42+ *
43+ * a) prevent any cache contention
44+ * b) allow as much concurrent access as possible (each shard is guarded by a
45+ * mutex)
46+ *
47+ * Topkeys passes on requests to the correct Shard (determined by the core id of
48+ * the calling thread), and when statistics are requested it aggregates
49+ * information from each shard. When aggregating information, the TopKeys class
50+ * has to remove duplicates from the possible pool of top keys because we shard
51+ * per core for performance. Previously, TopKeys would create 8 shards
52+ * (regardless of machine size), each with storage for a configurably amount of
53+ * keys and shard by key hash (which meant that we would not have duplicate keys
54+ * across shards). Now that we shard per core instead of by key hash, to keep
55+ * the size of the stat output the same we need a minimum of 8 X N keys per
56+ * shard (because each shard could be an exact duplicate of the others).
4057 *
4158 * === TopKeys::Shard ===
4259 *
86103 * contents is replaced by the incoming key. Finally the linked-list
87104 * is updated to move the updated element to the head of the list.
88105 */
89-
90- TopKeys::TopKeys ( int mkeys) {
106+ TopKeys::TopKeys ( int mkeys)
107+ : keys_to_return( mkeys * legacy_multiplier), shards(cb::get_cpu_count() ) {
91108 for (auto & shard : shards) {
92- shard. setMaxKeys (mkeys );
109+ shard-> setMaxKeys (keys_to_return );
93110 }
94111}
95112
@@ -123,14 +140,13 @@ ENGINE_ERROR_CODE TopKeys::json_stats(nlohmann::json& object,
123140 return ENGINE_SUCCESS;
124141}
125142
126- TopKeys::Shard& TopKeys::getShard (size_t key_hash) {
127- /* This is special-cased for 8 */
128- static_assert (NUM_SHARDS == 8 ,
129- " Topkeys::getShard() special-cased for SHARDS==8" );
130- return shards[key_hash & 0x7 ];
143+ TopKeys::Shard& TopKeys::getShard () {
144+ auto stripe =
145+ folly::AccessSpreader<std::atomic>::cachedCurrent (shards.size ());
146+ return *shards[stripe];
131147}
132148
133- TopKeys::Shard:: topkey_t * TopKeys::Shard::searchForKey (
149+ TopKeys::topkey_t * TopKeys::Shard::searchForKey (
134150 size_t key_hash, const cb::const_char_buffer& key) {
135151 for (auto & topkey : storage) {
136152 if (topkey.first .hash == key_hash) {
@@ -153,7 +169,7 @@ bool TopKeys::Shard::updateKey(const cb::const_char_buffer& key,
153169
154170 topkey_t * found_key = searchForKey (key_hash, key);
155171
156- if (found_key == NULL ) {
172+ if (! found_key) {
157173 // Key not found.
158174 if (storage.size () == max_keys) {
159175 // Re-use the lowest keys' storage.
@@ -202,29 +218,40 @@ void TopKeys::doUpdateKey(const void* key,
202218 }
203219
204220 try {
221+ // We store a key hash to make lookup of topkeys faster and because the
222+ // memory footprint is relatively small.
205223 cb::const_char_buffer key_buf (static_cast <const char *>(key), nkey);
206224 std::hash<cb::const_char_buffer> hash_fn;
207225 const size_t key_hash = hash_fn (key_buf);
208226
209- getShard (key_hash ).updateKey (key_buf, key_hash, operation_time);
227+ getShard ().updateKey (key_buf, key_hash, operation_time);
210228 } catch (const std::bad_alloc&) {
211229 // Failed to increment topkeys, continue...
212230 }
213231}
214232
215233struct tk_context {
234+ using CallbackFn = void (*)(const std::string&,
235+ const topkey_item_t &,
236+ void *);
216237 tk_context (const void * c,
217238 const AddStatFn& a,
218239 rel_time_t t,
219- nlohmann::json* arr)
220- : cookie(c), add_stat(a), current_time(t), array(arr) {
240+ nlohmann::json* arr,
241+ CallbackFn callbackFn)
242+ : cookie(c),
243+ add_stat (a),
244+ current_time(t),
245+ array(arr),
246+ callbackFunction(callbackFn) {
221247 // empty
222248 }
223249
224250 const void * cookie;
225251 AddStatFn add_stat;
226252 rel_time_t current_time;
227253 nlohmann::json* array;
254+ CallbackFn callbackFunction;
228255};
229256
230257static void tk_iterfunc (const std::string& key,
@@ -284,14 +311,23 @@ static void tk_jsonfunc(const std::string& key,
284311 c->array ->push_back (obj);
285312}
286313
314+ static void tk_aggregate_func (const TopKeys::topkey_t & it, void * arg) {
315+ auto * map = (std::unordered_map<std::string, topkey_item_t >*)arg;
316+
317+ auto res = map->insert (std::make_pair (it.first .key , it.second ));
318+
319+ // If insert failed, then we have a duplicate top key. Add the stats
320+ if (!res.second ) {
321+ res.first ->second .ti_access_count += it.second .ti_access_count ;
322+ }
323+ }
324+
287325ENGINE_ERROR_CODE TopKeys::doStats (const void * cookie,
288326 rel_time_t current_time,
289327 const AddStatFn& add_stat) {
290- struct tk_context context (cookie, add_stat, current_time, nullptr );
291-
292- for (auto & shard : shards) {
293- shard.accept_visitor (tk_iterfunc, &context);
294- }
328+ struct tk_context context (
329+ cookie, add_stat, current_time, nullptr , &tk_iterfunc);
330+ doStatsInner (context);
295331
296332 return ENGINE_SUCCESS;
297333}
@@ -309,21 +345,51 @@ ENGINE_ERROR_CODE TopKeys::doStats(const void* cookie,
309345ENGINE_ERROR_CODE TopKeys::do_json_stats (nlohmann::json& object,
310346 rel_time_t current_time) {
311347 nlohmann::json topkeys = nlohmann::json::array ();
312- struct tk_context context (nullptr , nullptr , current_time, &topkeys);
313-
314- /* Collate the topkeys JSON object */
315- for (auto & shard : shards) {
316- shard.accept_visitor (tk_jsonfunc, &context);
317- }
348+ struct tk_context context (
349+ nullptr , nullptr , current_time, &topkeys, &tk_jsonfunc);
350+ doStatsInner (context);
318351
352+ auto str = topkeys.dump ();
319353 object[" topkeys" ] = topkeys;
354+
320355 return ENGINE_SUCCESS;
321356}
322357
323358void TopKeys::Shard::accept_visitor (iterfunc_t visitor_func,
324359 void * visitor_ctx) {
325360 std::lock_guard<std::mutex> lock (mutex);
326- for (const auto key : list) {
327- visitor_func (key-> first . key , key-> second , visitor_ctx);
361+ for (const auto * key : list) {
362+ visitor_func (* key, visitor_ctx);
328363 }
329364}
365+
366+ void TopKeys::doStatsInner (const tk_context& stat_context) {
367+ // 1) Find the unique set of top keys by putting every top key in a map
368+ std::unordered_map<std::string, topkey_item_t > map =
369+ std::unordered_map<std::string, topkey_item_t >();
370+
371+ for (auto & shard : shards) {
372+ shard->accept_visitor (tk_aggregate_func, &map);
373+ }
374+
375+ // Easiest way to sort this by access_count is to drop the contents of the
376+ // map into a vector and sort that.
377+ auto items = std::vector<topkey_stat_t >(map.begin (), map.end ());
378+ std::sort (items.begin (),
379+ items.end (),
380+ [](const TopKeys::topkey_stat_t & a,
381+ const TopKeys::topkey_stat_t & b) {
382+ // Sort by number of accesses
383+ return a.second .ti_access_count > b.second .ti_access_count ;
384+ });
385+
386+ // 2) Iterate on this set making the required callback for each key. We only
387+ // iterate from the start of the container (highest access count) to the
388+ // number of keys to return.
389+ std::for_each (items.begin (),
390+ std::min (items.begin () + keys_to_return, items.end ()),
391+ [stat_context](const topkey_stat_t & t) {
392+ stat_context.callbackFunction (
393+ t.first , t.second , (void *)&stat_context);
394+ });
395+ }
0 commit comments